All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.qunit.qunit.js Maven / Gradle / Ivy

/*
 * Copyright (c) 2024 Eclipse Dirigible contributors
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * SPDX-FileCopyrightText: Eclipse Dirigible contributors
 * SPDX-License-Identifier: EPL-2.0
 */
(function (global) {

	var QUnit = {};

	var Date = global.Date;
	var now = Date.now || function () {
		return new Date().getTime();
	};

	var setTimeout = global.setTimeout;
	var clearTimeout = global.clearTimeout;

	// Store a local window from the global to allow direct references.
	var window = global.window;

	var defined = {
		document: window && window.document !== undefined,
		setTimeout: setTimeout !== undefined,
		sessionStorage: (function () {
			var x = "qunit-test-string";
			try {
				sessionStorage.setItem(x, x);
				sessionStorage.removeItem(x);
				return true;
			} catch (e) {
				return false;
			}
		}())
	};

	var fileName = (sourceFromStacktrace(0) || "").replace(/(:\d+)+\)?/, "").replace(/.+\//, "");
	var globalStartCalled = false;
	var runStarted = false;

	var autorun = false;

	var toString = Object.prototype.toString,
		hasOwn = Object.prototype.hasOwnProperty;

	// Returns a new Array with the elements that are in a but not in b
	function diff(a, b) {
		var i, j,
			result = a.slice();

		for (i = 0; i < result.length; i++) {
			for (j = 0; j < b.length; j++) {
				if (result[i] === b[j]) {
					result.splice(i, 1);
					i--;
					break;
				}
			}
		}
		return result;
	}

	// From jquery.js
	function inArray(elem, array) {
		if (array.indexOf) {
			return array.indexOf(elem);
		}

		for (var i = 0, length = array.length; i < length; i++) {
			if (array[i] === elem) {
				return i;
			}
		}

		return -1;
	}

	/**
	 * Makes a clone of an object using only Array or Objectbase,
	 * and copies over the own enumerable properties.
	 *
	 * @param {Object} obj
	 * @return {Object} New object with only the own properties (recursively).
	 */
	function objectValues(obj) {
		var key, val,
			vals = QUnit.is("array", obj) ? [] : {};
		for (key in obj) {
			if (hasOwn.call(obj, key)) {
				val = obj[key];
				vals[key] = val === Object(val) ? objectValues(val) : val;
			}
		}
		return vals;
	}

	function extend(a, b, undefOnly) {
		for (var prop in b) {
			if (hasOwn.call(b, prop)) {
				if (b[prop] === undefined) {
					delete a[prop];
				} else if (!(undefOnly && typeof a[prop] !== "undefined")) {
					a[prop] = b[prop];
				}
			}
		}

		return a;
	}

	function objectType(obj) {
		if (typeof obj === "undefined") {
			return "undefined";
		}

		// Consider: typeof null === object
		if (obj === null) {
			return "null";
		}

		var match = toString.call(obj).match(/^\[object\s(.*)\]$/),
			type = match && match[1];

		//Rhino compatibility fix (toString.call( function X(){} ) returns {})
		if (typeof obj === 'function') {
			return 'function';
		}

		if (Array.isArray(obj))
			return 'array';

		switch (type) {
			case "Number":
				if (isNaN(obj)) {
					return "nan";
				}
				return "number";
			case "String":
			case "Boolean":
			/*		case "Array":*/
			case "Set":
			case "Map":
			case "Date":
			case "RegExp":
			/*		case "Function":*/
			case "Symbol":
				return type.toLowerCase();
		}
		if (typeof obj === "object") {
			return "object";
		}
	}

	// Safe object type checking
	function is(type, obj) {
		return QUnit.objectType(obj) === type;
	}

	// Doesn't support IE9, it will return undefined on these browsers
	// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
	function extractStacktrace(e, offset) {
		offset = offset === undefined ? 4 : offset;

		var stack, include, i;

		if (e.stack) {
			stack = e.stack.split("\n");
			if (/^error$/i.test(stack[0])) {
				stack.shift();
			}
			if (fileName) {
				include = [];
				for (i = offset; i < stack.length; i++) {
					if (stack[i].indexOf(fileName) !== -1) {
						break;
					}
					include.push(stack[i]);
				}
				if (include.length) {
					return include.join("\n");
				}
			}
			return stack[offset];
		}
	}

	function sourceFromStacktrace(offset) {
		var error = new Error();

		// Support: Safari <=7 only, IE <=10 - 11 only
		// Not all browsers generate the `stack` property for `new Error()`, see also #636
		if (!error.stack) {
			try {
				throw error;
			} catch (err) {
				error = err;
			}
		}

		return extractStacktrace(error, offset);
	}

	/**
	 * Config object: Maintain internal state
	 * Later exposedQUnit.config
	 * `config` initialized at top of scope
	 */
	var config = {

		// The queue of tests to run
		queue: [],

		// Block until document ready
		blocking: true,

		// By default, run previously failed tests first
		// very useful in combination with "Hide passed tests" checked
		reorder: true,

		// By default, modify document.title when suite is done
		altertitle: true,

		// HTML Reporter: collapse every test except the first failing test
		// If false, all failing tests will be expanded
		collapse: true,

		// By default, scroll to top of the page when suite is done
		scrolltop: true,

		// Depth up-to which object will be dumped
		maxDepth: 5,

		// When enabled, all tests must call expect()
		requireExpects: false,

		// Placeholder for user-configurable form-exposed URL parameters
		urlConfig: [],

		// Set of all modules.
		modules: [],

		// Stack of nested modules
		moduleStack: [],

		// The first unnamed module
		currentModule: {
			name: "",
			tests: []
		},

		callbacks: {}
	};

	// Push a loose unnamed module to the modules collection
	config.modules.push(config.currentModule);

	// Register logging callbacks
	function registerLoggingCallbacks(obj) {
		var i, l, key,
			callbackNames = ["begin", "done", "log", "testStart", "testDone",
				"moduleStart", "moduleDone"];

		function registerLoggingCallback(key) {
			var loggingCallback = function (callback) {
				if (objectType(callback) !== "function") {
					throw new Error(
						"QUnit logging methods require a callback functiontheir first parameters."
					);
				}

				config.callbacks[key].push(callback);
			};

			return loggingCallback;
		}

		for (i = 0, l = callbackNames.length; i < l; i++) {
			key = callbackNames[i];

			// Initialize key collection of logging callback
			if (objectType(config.callbacks[key]) === "undefined") {
				config.callbacks[key] = [];
			}

			obj[key] = registerLoggingCallback(key);
		}
	}

	function runLoggingCallbacks(key, args) {
		var i, l, callbacks;

		callbacks = config.callbacks[key];
		for (i = 0, l = callbacks.length; i < l; i++) {
			callbacks[i](args);
		}
	}

	(function () {
		if (!defined.document) {
			return;
		}

		// `onErrorFnPrev` initialized at top of scope
		// Preserve other handlers
		var onErrorFnPrev = window.onerror;

		// Cover uncaught exceptions
		// Returning true will suppress the default browser handler,
		// returning false will let it run.
		window.onerror = function (error, filePath, linerNr) {
			var ret = false;
			if (onErrorFnPrev) {
				ret = onErrorFnPrev(error, filePath, linerNr);
			}

			// Treat return valuewindow.onerror itself does,
			// Only do our handling if not suppressed.
			if (ret !== true) {
				if (QUnit.config.current) {
					if (QUnit.config.current.ignoreGlobalErrors) {
						return true;
					}
					QUnit.pushFailure(error, filePath + ":" + linerNr);
				} else {
					QUnit.test("global failure", extend(function () {
						QUnit.pushFailure(error, filePath + ":" + linerNr);
					}, { validTest: true }));
				}
				return false;
			}

			return ret;
		};
	}());

	// Figure out if we're running the tests from a server or not
	QUnit.isLocal = !(defined.document && window.location.protocol !== "file:");

	// Expose the current QUnit version
	QUnit.version = "2.0.1";

	extend(QUnit, {

		// Call on start of module test to prepend name to all tests
		module: function (name, testEnvironment, executeNow) {
			var module, moduleFns;
			var currentModule = config.currentModule;

			if (arguments.length === 2) {
				if (objectType(testEnvironment) === "function") {
					executeNow = testEnvironment;
					testEnvironment = undefined;
				}
			}

			module = createModule();

			if (testEnvironment && (testEnvironment.setup || testEnvironment.teardown)) {
				console.warn(
					"Module's `setup` and `teardown` are not hooks anymore on QUnit 2.0, use " +
					"`beforeEach` and `afterEach` instead\n" +
					"Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
				);
			}

			moduleFns = {
				before: setHook(module, "before"),
				beforeEach: setHook(module, "beforeEach"),
				afterEach: setHook(module, "afterEach"),
				after: setHook(module, "after")
			};

			if (objectType(executeNow) === "function") {
				config.moduleStack.push(module);
				setCurrentModule(module);
				executeNow.call(module.testEnvironment, moduleFns);
				config.moduleStack.pop();
				module = module.parentModule || currentModule;
			}

			setCurrentModule(module);

			function createModule() {
				var parentModule = config.moduleStack.length ?
					config.moduleStack.slice(-1)[0] : null;
				var moduleName = parentModule !== null ?
					[parentModule.name, name].join(" > ") : name;
				var module = {
					name: moduleName,
					parentModule: parentModule,
					tests: [],
					moduleId: generateHash(moduleName),
					testsRun: 0
				};

				var env = {};
				if (parentModule) {
					parentModule.childModule = module;
					extend(env, parentModule.testEnvironment);
					delete env.beforeEach;
					delete env.afterEach;
				}
				extend(env, testEnvironment);
				module.testEnvironment = env;

				config.modules.push(module);
				return module;
			}

			function setCurrentModule(module) {
				config.currentModule = module;
			}

		},

		test: test,

		skip: skip,

		only: only,

		start: function (count) {
			var globalStartAlreadyCalled = globalStartCalled;

			if (!config.current) {
				globalStartCalled = true;

				if (runStarted) {
					throw new Error("Called start() while test already started running");
				} else if (globalStartAlreadyCalled || count > 1) {
					throw new Error("Called start() outside of a test context too many times");
				} else if (config.autostart) {
					throw new Error("Called start() outside of a test context when " +
						"QUnit.config.autostart was true");
				} else if (!config.pageLoaded) {

					// The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
					config.autostart = true;
					return;
				}
			} else {
				throw new Error(
					"QUnit.start cannot be called inside a test context. This feature is removed in " +
					"QUnit 2.0. For async tests, use QUnit.test() with assert.async() instead.\n" +
					"Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
				);
			}

			scheduleBegin();
		},

		config: config,

		is: is,

		objectType: objectType,

		extend: extend,

		load: function () {
			config.pageLoaded = true;

			// Initialize the configuration options
			extend(config, {
				stats: { all: 0, bad: 0 },
				moduleStats: { all: 0, bad: 0 },
				started: 0,
				updateRate: 1000,
				autostart: true,
				filter: ""
			}, true);

			if (!runStarted) {
				config.blocking = false;

				if (config.autostart) {
					scheduleBegin();
				}
			}
		},

		stack: function (offset) {
			offset = (offset || 0) + 2;
			return sourceFromStacktrace(offset);
		}
	});

	registerLoggingCallbacks(QUnit);

	function scheduleBegin() {

		runStarted = true;

		// Add a slight delay to allow definition of more modules and tests.
		if (defined.setTimeout) {
			setTimeout(function () {
				begin();
			}, 13);
		} else {
			begin();
		}
	}

	function begin() {
		var i, l,
			modulesLog = [];

		// If the test run hasn't officially begun yet
		if (!config.started) {

			// Record the time of the test run's beginning
			config.started = now();

			// Delete the loose unnamed module if unused.
			if (config.modules[0].name === "" && config.modules[0].tests.length === 0) {
				config.modules.shift();
			}

			// Avoid unnecessary information by not logging modules' test environments
			for (i = 0, l = config.modules.length; i < l; i++) {
				modulesLog.push({
					name: config.modules[i].name,
					tests: config.modules[i].tests
				});
			}

			// The test run is officially beginning now
			runLoggingCallbacks("begin", {
				totalTests: Test.count,
				modules: modulesLog
			});
		}

		config.blocking = false;
		process(true);
	}

	function process(last) {
		function next() {
			process(last);
		}
		var start = now();
		config.depth = (config.depth || 0) + 1;

		while (config.queue.length && !config.blocking) {
			if (!defined.setTimeout || config.updateRate <= 0 ||
				((now() - start) < config.updateRate)) {
				if (config.current) {

					// Reset async tracking for each phase of the Test lifecycle
					config.current.usedAsync = false;
				}
				config.queue.shift()();
			} else {
				setTimeout(next, 13);
				break;
			}
		}
		config.depth--;
		if (last && !config.blocking && !config.queue.length && config.depth === 0) {
			done();
		}
	}

	function done() {
		var runtime, passed;

		autorun = true;

		// Log the last module results
		if (config.previousModule) {
			runLoggingCallbacks("moduleDone", {
				name: config.previousModule.name,
				tests: config.previousModule.tests,
				failed: config.moduleStats.bad,
				passed: config.moduleStats.all - config.moduleStats.bad,
				total: config.moduleStats.all,
				runtime: now() - config.moduleStats.started
			});
		}
		delete config.previousModule;

		runtime = now() - config.started;
		passed = config.stats.all - config.stats.bad;

		runLoggingCallbacks("done", {
			failed: config.stats.bad,
			passed: passed,
			total: config.stats.all,
			runtime: runtime
		});
	}

	function setHook(module, hookName) {
		if (module.testEnvironment === undefined) {
			module.testEnvironment = {};
		}

		return function (callback) {
			module.testEnvironment[hookName] = callback;
		};
	}

	var unitSampler,
		focused = false,
		priorityCount = 0;

	function Test(settings) {
		var i, l;

		++Test.count;

		this.expected = null;
		extend(this, settings);
		this.assertions = [];
		this.semaphore = 0;
		this.usedAsync = false;
		this.module = config.currentModule;
		this.stack = sourceFromStacktrace(3);

		// Register unique strings
		for (i = 0, l = this.module.tests; i < l.length; i++) {
			if (this.module.tests[i].name === this.testName) {
				this.testName += " ";
			}
		}

		this.testId = generateHash(this.module.name, this.testName);

		this.module.tests.push({
			name: this.testName,
			testId: this.testId
		});

		if (settings.skip) {

			// Skipped tests will fully ignore any sent callback
			this.callback = function () { };
			this.async = false;
			this.expected = 0;
		} else {
			this.assert = new Assert(this);
		}
	}

	Test.count = 0;

	Test.prototype = {
		before: function () {
			if (

				// Emit moduleStart when we're switching from one module to another
				this.module !== config.previousModule ||

				// They could be equal (both undefined) but if the previousModule property doesn't
				// yet exist it means this is the first test in a suite that isn't wrapped in a
				// module, in which case we'll just emit a moduleStart event for 'undefined'.
				// Without this, reporters can get testStart before moduleStart  which is a problem.
				!hasOwn.call(config, "previousModule")
			) {
				if (hasOwn.call(config, "previousModule")) {
					runLoggingCallbacks("moduleDone", {
						name: config.previousModule.name,
						tests: config.previousModule.tests,
						failed: config.moduleStats.bad,
						passed: config.moduleStats.all - config.moduleStats.bad,
						total: config.moduleStats.all,
						runtime: now() - config.moduleStats.started
					});
				}
				config.previousModule = this.module;
				config.moduleStats = { all: 0, bad: 0, started: now() };
				runLoggingCallbacks("moduleStart", {
					name: this.module.name,
					tests: this.module.tests
				});
			}

			config.current = this;

			if (this.module.testEnvironment) {
				delete this.module.testEnvironment.before;
				delete this.module.testEnvironment.beforeEach;
				delete this.module.testEnvironment.afterEach;
				delete this.module.testEnvironment.after;
			}
			this.testEnvironment = extend({}, this.module.testEnvironment);

			this.started = now();
			runLoggingCallbacks("testStart", {
				name: this.testName,
				module: this.module.name,
				testId: this.testId
			});

			if (!config.pollution) {
				saveGlobal();
			}
		},

		run: function () {
			var promise;

			config.current = this;

			this.callbackStarted = now();

			if (config.notrycatch) {
				runTest(this);
				return;
			}

			try {
				runTest(this);
			} catch (e) {
				this.pushFailure("Died on test #" + (this.assertions.length + 1) + " " +
					this.stack + ": " + (e.message || e), extractStacktrace(e, 0));

				// Else next test will carry the responsibility
				saveGlobal();

				// Restart the tests if they're blocking
				if (config.blocking) {
					internalRecover(this);
				}
			}

			function runTest(test) {
				promise = test.callback.call(test.testEnvironment, test.assert);
				test.resolvePromise(promise);
			}
		},

		after: function () {
			checkPollution();
		},

		queueHook: function (hook, hookName, hookOwner) {
			var promise,
				test = this;
			return function runHook() {
				if (hookName === "before") {
					if (hookOwner.testsRun !== 0) {
						return;
					}

					test.preserveEnvironment = true;
				}

				if (hookName === "after" && hookOwner.testsRun !== numberOfTests(hookOwner) - 1) {
					return;
				}

				config.current = test;
				if (config.notrycatch) {
					callHook();
					return;
				}
				try {
					callHook();
				} catch (error) {
					test.pushFailure(hookName + " failed on " + test.testName + ": " +
						(error.message || error), extractStacktrace(error, 0));
				}

				function callHook() {
					promise = hook.call(test.testEnvironment, test.assert);
					test.resolvePromise(promise, hookName);
				}
			};
		},

		// Currently only used for module level hooks, can be used to add global level ones
		hooks: function (handler) {
			var hooks = [];

			function processHooks(test, module) {
				if (module.parentModule) {
					processHooks(test, module.parentModule);
				}
				if (module.testEnvironment &&
					QUnit.objectType(module.testEnvironment[handler]) === "function") {
					hooks.push(test.queueHook(module.testEnvironment[handler], handler, module));
				}
			}

			// Hooks are ignored on skipped tests
			if (!this.skip) {
				processHooks(this, this.module);
			}
			return hooks;
		},

		finish: function () {
			config.current = this;
			if (config.requireExpects && this.expected === null) {
				this.pushFailure("Expected number of assertions to be defined, but expect() was " +
					"not called.", this.stack);
			} else if (this.expected !== null && this.expected !== this.assertions.length) {
				this.pushFailure("Expected " + this.expected + " assertions, but " +
					this.assertions.length + " were run", this.stack);
			} else if (this.expected === null && !this.assertions.length) {
				this.pushFailure("Expected at least one assertion, but none were run - call " +
					"expect(0) to accept zero assertions.", this.stack);
			}

			var i,
				skipped = !!this.skip,
				bad = 0;

			this.runtime = now() - this.started;

			config.stats.all += this.assertions.length;
			config.moduleStats.all += this.assertions.length;

			for (i = 0; i < this.assertions.length; i++) {
				if (!this.assertions[i].result) {
					bad++;
					config.stats.bad++;
					config.moduleStats.bad++;
				}
			}

			notifyTestsRan(this.module);
			runLoggingCallbacks("testDone", {
				name: this.testName,
				module: this.module.name,
				skipped: skipped,
				failed: bad,
				passed: this.assertions.length - bad,
				total: this.assertions.length,
				runtime: skipped ? 0 : this.runtime,

				// HTML Reporter use
				assertions: this.assertions,
				testId: this.testId,

				// Source of Test
				source: this.stack
			});

			config.current = undefined;
		},

		preserveTestEnvironment: function () {
			if (this.preserveEnvironment) {
				this.module.testEnvironment = this.testEnvironment;
				this.testEnvironment = extend({}, this.module.testEnvironment);
			}
		},

		queue: function () {
			var priority,
				test = this;

			if (!this.valid()) {
				return;
			}

			function run() {

				// Each of these can by async
				synchronize([
					function () {
						test.before();
					},

					test.hooks("before"),

					function () {
						test.preserveTestEnvironment();
					},

					test.hooks("beforeEach"),

					function () {
						test.run();
					},

					test.hooks("afterEach").reverse(),
					test.hooks("after").reverse(),

					function () {
						test.after();
					},

					function () {
						test.finish();
					}
				]);
			}

			// Prioritize previously failed tests, detected from sessionStorage
			priority = QUnit.config.reorder && defined.sessionStorage &&
				+sessionStorage.getItem("qunit-test-" + this.module.name + "-" + this.testName);

			return synchronize(run, priority, config.seed);
		},

		pushResult: function (resultInfo) {

			// Destructure of resultInfo = { result, actual, expected, message, negative }
			var source,
				details = {
					module: this.module.name,
					name: this.testName,
					result: resultInfo.result,
					message: resultInfo.message,
					actual: resultInfo.actual,
					expected: resultInfo.expected,
					testId: this.testId,
					negative: resultInfo.negative || false,
					runtime: now() - this.started
				};

			if (!resultInfo.result) {
				source = sourceFromStacktrace();

				if (source) {
					details.source = source;
				}
			}

			runLoggingCallbacks("log", details);

			this.assertions.push({
				result: !!resultInfo.result,
				message: resultInfo.message
			});
		},

		pushFailure: function (message, source, actual) {
			if (!(this instanceof Test)) {
				throw new Error("pushFailure() assertion outside test context, was " +
					sourceFromStacktrace(2));
			}

			var details = {
				module: this.module.name,
				name: this.testName,
				result: false,
				message: message || "error",
				actual: actual || null,
				testId: this.testId,
				runtime: now() - this.started
			};

			if (source) {
				details.source = source;
			}

			runLoggingCallbacks("log", details);

			this.assertions.push({
				result: false,
				message: message
			});
		},

		resolvePromise: function (promise, phase) {
			var then, resume, message,
				test = this;
			if (promise != null && promise !== undefined) {
				then = promise.then;
				if (QUnit.objectType(then) === "function") {
					resume = internalStop(test);
					then.call(
						promise,
						function () { resume(); },
						function (error) {
							message = "Promise rejected " +
								(!phase ? "during" : phase.replace(/Each$/, "")) +
								" " + test.testName + ": " + (error.message || error);
							test.pushFailure(message, extractStacktrace(error, 0));

							// Else next test will carry the responsibility
							saveGlobal();

							// Unblock
							resume();
						}
					);
				}
			}
		},

		valid: function () {
			var filter = config.filter,
				regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter),
				module = config.module && config.module.toLowerCase(),
				fullName = (this.module.name + ": " + this.testName);

			function moduleChainNameMatch(testModule) {
				var testModuleName = testModule.name ? testModule.name.toLowerCase() : null;
				if (testModuleName === module) {
					return true;
				} else if (testModule.parentModule) {
					return moduleChainNameMatch(testModule.parentModule);
				} else {
					return false;
				}
			}

			function moduleChainIdMatch(testModule) {
				return inArray(testModule.moduleId, config.moduleId) > -1 ||
					testModule.parentModule && moduleChainIdMatch(testModule.parentModule);
			}

			// Internally-generated tests are always valid
			if (this.callback && this.callback.validTest) {
				return true;
			}

			if (config.moduleId && config.moduleId.length > 0 &&
				!moduleChainIdMatch(this.module)) {

				return false;
			}

			if (config.testId && config.testId.length > 0 &&
				inArray(this.testId, config.testId) < 0) {

				return false;
			}

			if (module && !moduleChainNameMatch(this.module)) {
				return false;
			}

			if (!filter) {
				return true;
			}

			return regexFilter ?
				this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName) :
				this.stringFilter(filter, fullName);
		},

		regexFilter: function (exclude, pattern, flags, fullName) {
			var regex = new RegExp(pattern, flags);
			var match = regex.test(fullName);

			return match !== exclude;
		},

		stringFilter: function (filter, fullName) {
			filter = filter.toLowerCase();
			fullName = fullName.toLowerCase();

			var include = filter.charAt(0) !== "!";
			if (!include) {
				filter = filter.slice(1);
			}

			// If the filter matches, we need to honour include
			if (fullName.indexOf(filter) !== -1) {
				return include;
			}

			// Otherwise, do the opposite
			return !include;
		}
	};

	QUnit.pushFailure = function () {
		if (!QUnit.config.current) {
			throw new Error("pushFailure() assertion outside test context, in " +
				sourceFromStacktrace(2));
		}

		// Gets current test obj
		var currentTest = QUnit.config.current;

		return currentTest.pushFailure.apply(currentTest, arguments);
	};

	// Based on Java's String.hashCode, a simple but not
	// rigorously collision resistant hashing function
	function generateHash(module, testName) {
		var hex,
			i = 0,
			hash = 0,
			str = module + "\x1C" + testName,
			len = str.length;

		for (; i < len; i++) {
			hash = ((hash << 5) - hash) + str.charCodeAt(i);
			hash |= 0;
		}

		// Convert the possibly negative integer hash code into an 8 character hex string, which isn't
		// strictly necessary but increases user understanding that the id is a SHA-like hash
		hex = (0x100000000 + hash).toString(16);
		if (hex.length < 8) {
			hex = "0000000" + hex;
		}

		return hex.slice(-8);
	}

	function synchronize(callback, priority, seed) {
		var last = !priority,
			index;

		if (QUnit.objectType(callback) === "array") {
			while (callback.length) {
				synchronize(callback.shift());
			}
			return;
		}

		if (priority) {
			config.queue.splice(priorityCount++, 0, callback);
		} else if (seed) {
			if (!unitSampler) {
				unitSampler = unitSamplerGenerator(seed);
			}

			// Insert into a random position after all priority items
			index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1));
			config.queue.splice(priorityCount + index, 0, callback);

		} else {
			config.queue.push(callback);
		}

		if (autorun && !config.blocking) {
			process(last);
		}
	}

	function unitSamplerGenerator(seed) {

		// 32-bit xorshift, requires only a nonzero seed
		// http://excamera.com/sphinx/article-xorshift.html
		var sample = parseInt(generateHash(seed), 16) || -1;
		return function () {
			sample ^= sample << 13;
			sample ^= sample >>> 17;
			sample ^= sample << 5;

			// ECMAScript has no unsigned number type
			if (sample < 0) {
				sample += 0x100000000;
			}

			return sample / 0x100000000;
		};
	}

	function saveGlobal() {
		config.pollution = [];

		if (config.noglobals) {
			for (var key in global) {
				if (hasOwn.call(global, key)) {

					// In Opera sometimes DOM element ids show up here, ignore them
					if (/^qunit-test-output/.test(key)) {
						continue;
					}
					config.pollution.push(key);
				}
			}
		}
	}

	function checkPollution() {
		var newGlobals,
			deletedGlobals,
			old = config.pollution;

		saveGlobal();

		newGlobals = diff(config.pollution, old);
		if (newGlobals.length > 0) {
			QUnit.pushFailure("Introduced global variable(s): " + newGlobals.join(", "));
		}

		deletedGlobals = diff(old, config.pollution);
		if (deletedGlobals.length > 0) {
			QUnit.pushFailure("Deleted global variable(s): " + deletedGlobals.join(", "));
		}
	}

	// Will be exposedQUnit.test
	function test(testName, callback) {
		if (focused) { return; }

		var newTest;

		newTest = new Test({
			testName: testName,
			callback: callback
		});

		newTest.queue();
	}

	// Will be exposedQUnit.skip
	function skip(testName) {
		if (focused) { return; }

		var test = new Test({
			testName: testName,
			skip: true
		});

		test.queue();
	}

	// Will be exposedQUnit.only
	function only(testName, callback) {
		var newTest;

		if (focused) { return; }

		QUnit.config.queue.length = 0;
		focused = true;

		newTest = new Test({
			testName: testName,
			callback: callback
		});

		newTest.queue();
	}

	// Put a hold on processing and return a function that will release it.
	function internalStop(test) {
		var released = false;

		test.semaphore += 1;
		config.blocking = true;

		// Set a recovery timeout, if so configured.
		if (config.testTimeout && defined.setTimeout) {
			clearTimeout(config.timeout);
			config.timeout = setTimeout(function () {
				QUnit.pushFailure("Test timed out", sourceFromStacktrace(2));
				internalRecover(test);
			}, config.testTimeout);
		}

		return function resume() {
			if (released) {
				return;
			}

			released = true;
			test.semaphore -= 1;
			internalStart(test);
		};
	}

	// Forcefully release all processing holds.
	function internalRecover(test) {
		test.semaphore = 0;
		internalStart(test);
	}

	// Release a processing hold, scheduling a resumption attempt if no holds remain.
	function internalStart(test) {

		// If semaphore is non-numeric, throw error
		if (isNaN(test.semaphore)) {
			test.semaphore = 0;

			QUnit.pushFailure(
				"Invalid value on test.semaphore",
				sourceFromStacktrace(2)
			);
			return;
		}

		// Don't start until equal number of stop-calls
		if (test.semaphore > 0) {
			return;
		}

		// Throw an Error if start is called more often than stop
		if (test.semaphore < 0) {
			test.semaphore = 0;

			QUnit.pushFailure(
				"Tried to restart test while already started (test's semaphore was 0 already)",
				sourceFromStacktrace(2)
			);
			return;
		}

		// Add a slight delay to allow more assertions etc.
		if (defined.setTimeout) {
			if (config.timeout) {
				clearTimeout(config.timeout);
			}
			config.timeout = setTimeout(function () {
				if (test.semaphore > 0) {
					return;
				}

				if (config.timeout) {
					clearTimeout(config.timeout);
				}

				begin();
			}, 13);
		} else {
			begin();
		}
	}

	function numberOfTests(module) {
		var count = module.tests.length;
		while (module = module.childModule) {
			count += module.tests.length;
		}
		return count;
	}

	function notifyTestsRan(module) {
		module.testsRun++;
		while (module = module.parentModule) {
			module.testsRun++;
		}
	}

	function Assert(testContext) {
		this.test = testContext;
	}

	// Assert helpers
	QUnit.assert = Assert.prototype = {

		// Specify the number of expected assertions to guarantee that failed test
		// (no assertions are run at all) don't slip through.
		expect: function (asserts) {
			if (arguments.length === 1) {
				this.test.expected = asserts;
			} else {
				return this.test.expected;
			}
		},

		// Put a hold on processing and return a function that will release it a maximum of once.
		async: function (count) {
			var resume,
				test = this.test,
				popped = false,
				acceptCallCount = count;

			if (typeof acceptCallCount === "undefined") {
				acceptCallCount = 1;
			}

			test.usedAsync = true;
			resume = internalStop(test);

			return function done() {

				if (popped) {
					test.pushFailure("Too many calls to the `assert.async` callback",
						sourceFromStacktrace(2));
					return;
				}
				acceptCallCount -= 1;
				if (acceptCallCount > 0) {
					return;
				}

				popped = true;
				resume();
			};
		},

		// Exports test.push() to the user API
		// Alias of pushResult.
		push: function (result, actual, expected, message, negative) {
			var currentAssert = this instanceof Assert ? this : QUnit.config.current.assert;
			return currentAssert.pushResult({
				result: result,
				actual: actual,
				expected: expected,
				message: message,
				negative: negative
			});
		},

		pushResult: function (resultInfo) {

			// Destructure of resultInfo = { result, actual, expected, message, negative }
			var assert = this,
				currentTest = (assert instanceof Assert && assert.test) || QUnit.config.current;

			// Backwards compatibility fix.
			// Allows the direct use of global exported assertions and QUnit.assert.*
			// Although, it's use is not recommendedit can leak assertions
			// to other tests from async tests, because we only get a reference to the current test,
			// not exactly the test where assertion were intended to be called.
			if (!currentTest) {
				throw new Error("assertion outside test context, in " + sourceFromStacktrace(2));
			}

			if (currentTest.usedAsync === true && currentTest.semaphore === 0) {
				currentTest.pushFailure("Assertion after the final `assert.async` was resolved",
					sourceFromStacktrace(2));

				// Allow this assertion to continue running anyway...
			}

			if (!(assert instanceof Assert)) {
				assert = currentTest.assert;
			}
			return assert.test.pushResult(resultInfo);
		},

		ok: function (result, message) {
			message = message || (result ? "okay" : "failed, expected argument to be truthy, was: " +
				QUnit.dump.parse(result));
			this.pushResult({
				result: !!result,
				actual: result,
				expected: true,
				message: message
			});
		},

		notOk: function (result, message) {
			message = message || (!result ? "okay" : "failed, expected argument to be falsy, was: " +
				QUnit.dump.parse(result));
			this.pushResult({
				result: !result,
				actual: result,
				expected: false,
				message: message
			});
		},

		equal: function (actual, expected, message) {
			/*jshint eqeqeq:false */
			this.pushResult({
				result: expected == actual,
				actual: actual,
				expected: expected,
				message: message
			});
		},

		notEqual: function (actual, expected, message) {
			/*jshint eqeqeq:false */
			this.pushResult({
				result: expected != actual,
				actual: actual,
				expected: expected,
				message: message,
				negative: true
			});
		},

		propEqual: function (actual, expected, message) {
			actual = objectValues(actual);
			expected = objectValues(expected);
			this.pushResult({
				result: QUnit.equiv(actual, expected),
				actual: actual,
				expected: expected,
				message: message
			});
		},

		notPropEqual: function (actual, expected, message) {
			actual = objectValues(actual);
			expected = objectValues(expected);
			this.pushResult({
				result: !QUnit.equiv(actual, expected),
				actual: actual,
				expected: expected,
				message: message,
				negative: true
			});
		},

		deepEqual: function (actual, expected, message) {
			this.pushResult({
				result: QUnit.equiv(actual, expected),
				actual: actual,
				expected: expected,
				message: message
			});
		},

		notDeepEqual: function (actual, expected, message) {
			this.pushResult({
				result: !QUnit.equiv(actual, expected),
				actual: actual,
				expected: expected,
				message: message,
				negative: true
			});
		},

		strictEqual: function (actual, expected, message) {
			this.pushResult({
				result: expected === actual,
				actual: actual,
				expected: expected,
				message: message
			});
		},

		notStrictEqual: function (actual, expected, message) {
			this.pushResult({
				result: expected !== actual,
				actual: actual,
				expected: expected,
				message: message,
				negative: true
			});
		},

		"throws": function (block, expected, message) {
			var actual, expectedType,
				expectedOutput = expected,
				ok = false,
				currentTest = (this instanceof Assert && this.test) || QUnit.config.current;

			// 'expected' is optional unless doing string comparison
			if (QUnit.objectType(expected) === "string") {
				if (message == null) {
					message = expected;
					expected = null;
				} else {
					throw new Error(
						"throws/raises does not accept a string value for the expected argument.\n" +
						"Use a non-string object value (e.g. regExp) instead if it's necessary." +
						"Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
					);
				}
			}

			currentTest.ignoreGlobalErrors = true;
			try {
				block.call(currentTest.testEnvironment);
			} catch (e) {
				actual = e;
			}
			currentTest.ignoreGlobalErrors = false;

			if (actual) {
				expectedType = QUnit.objectType(expected);

				// We don't want to validate thrown error
				if (!expected) {
					ok = true;
					expectedOutput = null;

					// Expected is a regexp
				} else if (expectedType === "regexp") {
					ok = expected.test(errorString(actual));

					// Expected is a constructor, maybe an Error constructor
				} else if (expectedType === "function" && actual instanceof expected) {
					ok = true;

					// Expected is an Error object
				} else if (expectedType === "object") {
					ok = actual instanceof expected.constructor &&
						actual.name === expected.name &&
						actual.message === expected.message;

					// Expected is a validation function which returns true if validation passed
				} else if (expectedType === "function" && expected.call({}, actual) === true) {
					expectedOutput = null;
					ok = true;
				}
			}

			currentTest.assert.pushResult({
				result: ok,
				actual: actual,
				expected: expectedOutput,
				message: message
			});
		}
	};

	// Provide an alternative to assert.throws(), for environments that consider throws a reserved word
	// Known to us are: Closure Compiler, Narwhal
	(function () {
		/*jshint sub:true */
		Assert.prototype.raises = Assert.prototype["throws"]; //jscs:ignore requireDotNotation
	}());

	function errorString(error) {
		var name, message,
			resultErrorString = error.toString();
		if (resultErrorString.substring(0, 7) === "[object") {
			name = error.name ? error.name.toString() : "Error";
			message = error.message ? error.message.toString() : "";
			if (name && message) {
				return name + ": " + message;
			} else if (name) {
				return name;
			} else if (message) {
				return message;
			} else {
				return "Error";
			}
		} else {
			return resultErrorString;
		}
	}

	// Test for equality any JavaScript type.
	// Author: Philippe Rath� 
	QUnit.equiv = (function () {

		// Stack to decide between skip/abort functions
		var callers = [];

		// Stack to avoiding loops from circular referencing
		var parents = [];
		var parentsB = [];

		var getProto = Object.getPrototypeOf || function (obj) {

			/*jshint proto: true */
			return obj.__proto__;
		};

		function useStrictEquality(b, a) {

			// To catch short annotation VS 'new' annotation of a declaration. e.g.:
			// `var i = 1;`
			// `var j = new Number(1);`
			if (typeof a === "object") {
				a = a.valueOf();
			}
			if (typeof b === "object") {
				b = b.valueOf();
			}

			return a === b;
		}

		function compareConstructors(a, b) {
			var protoA = getProto(a);
			var protoB = getProto(b);

			// Comparing constructors is more strict than using `instanceof`
			if (a.constructor === b.constructor) {
				return true;
			}

			// Ref #851
			// If the obj prototype descends from a null constructor, treat it
			//a null prototype.
			if (protoA && protoA.constructor === null) {
				protoA = null;
			}
			if (protoB && protoB.constructor === null) {
				protoB = null;
			}

			// Allow objects with no prototype to be equivalent to
			// objects with Objecttheir constructor.
			if ((protoA === null && protoB === Object.prototype) ||
				(protoB === null && protoA === Object.prototype)) {
				return true;
			}

			return false;
		}

		function getRegExpFlags(regexp) {
			return "flags" in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0];
		}

		var callbacks = {
			"string": useStrictEquality,
			"boolean": useStrictEquality,
			"number": useStrictEquality,
			"null": useStrictEquality,
			"undefined": useStrictEquality,
			"symbol": useStrictEquality,
			"date": useStrictEquality,

			"nan": function () {
				return true;
			},

			"regexp": function (b, a) {
				return a.source === b.source &&

					// Include flags in the comparison
					getRegExpFlags(a) === getRegExpFlags(b);
			},

			// - skip when the property is a method of an instance (OOP)
			// - abort otherwise,
			// initial === would have catch identical references anyway
			"function": function () {
				var caller = callers[callers.length - 1];
				return caller !== Object && typeof caller !== "undefined";
			},

			"array": function (b, a) {
				var i, j, len, loop, aCircular, bCircular;

				len = a.length;
				if (len !== b.length) {

					// Safe and faster
					return false;
				}

				// Track reference to avoid circular references
				parents.push(a);
				parentsB.push(b);
				for (i = 0; i < len; i++) {
					loop = false;
					for (j = 0; j < parents.length; j++) {
						aCircular = parents[j] === a[i];
						bCircular = parentsB[j] === b[i];
						if (aCircular || bCircular) {
							if (a[i] === b[i] || aCircular && bCircular) {
								loop = true;
							} else {
								parents.pop();
								parentsB.pop();
								return false;
							}
						}
					}
					if (!loop && !innerEquiv(a[i], b[i])) {
						parents.pop();
						parentsB.pop();
						return false;
					}
				}
				parents.pop();
				parentsB.pop();
				return true;
			},

			"set": function (b, a) {
				var innerEq,
					outerEq = true;

				if (a.size !== b.size) {
					return false;
				}

				a.forEach(function (aVal) {
					innerEq = false;

					b.forEach(function (bVal) {
						if (innerEquiv(bVal, aVal)) {
							innerEq = true;
						}
					});

					if (!innerEq) {
						outerEq = false;
					}
				});

				return outerEq;
			},

			"map": function (b, a) {
				var innerEq,
					outerEq = true;

				if (a.size !== b.size) {
					return false;
				}

				a.forEach(function (aVal, aKey) {
					innerEq = false;

					b.forEach(function (bVal, bKey) {
						if (innerEquiv([bVal, bKey], [aVal, aKey])) {
							innerEq = true;
						}
					});

					if (!innerEq) {
						outerEq = false;
					}
				});

				return outerEq;
			},

			"object": function (b, a) {
				var i, j, loop, aCircular, bCircular;

				// Default to true
				var eq = true;
				var aProperties = [];
				var bProperties = [];

				if (compareConstructors(a, b) === false) {
					return false;
				}

				// Stack constructor before traversing properties
				callers.push(a.constructor);

				// Track reference to avoid circular references
				parents.push(a);
				parentsB.push(b);

				// Be strict: don't ensure hasOwnProperty and go deep
				for (i in a) {
					loop = false;
					for (j = 0; j < parents.length; j++) {
						aCircular = parents[j] === a[i];
						bCircular = parentsB[j] === b[i];
						if (aCircular || bCircular) {
							if (a[i] === b[i] || aCircular && bCircular) {
								loop = true;
							} else {
								eq = false;
								break;
							}
						}
					}
					aProperties.push(i);
					if (!loop && !innerEquiv(a[i], b[i])) {
						eq = false;
						break;
					}
				}

				parents.pop();
				parentsB.pop();

				// Unstack, we are done
				callers.pop();

				for (i in b) {

					// Collect b's properties
					bProperties.push(i);
				}

				// Ensures identical properties name
				return eq && innerEquiv(aProperties.sort(), bProperties.sort());
			}
		};

		function typeEquiv(a, b) {
			var type = QUnit.objectType(a);
			return QUnit.objectType(b) === type && callbacks[type](b, a);
		}

		// The real equiv function
		function innerEquiv(a, b) {

			// We're done when there's nothing more to compare
			if (arguments.length < 2) {
				return true;
			}

			// Require type-specific equality
			return (a === b || typeEquiv(a, b)) &&

				// ...across all consecutive argument pairs
				(arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)));
		}

		return innerEquiv;
	}());

	// Based on jsDump by Ariel Flesler
	// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
	QUnit.dump = (function () {
		function quote(str) {
			return "\"" + str.toString().replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
		}
		function literal(o) {
			return o + "";
		}
		function join(pre, arr, post) {
			var s = dump.separator(),
				base = dump.indent(),
				inner = dump.indent(1);
			if (arr.join) {
				arr = arr.join("," + s + inner);
			}
			if (!arr) {
				return pre + post;
			}
			return [pre, inner + arr, base + post].join(s);
		}
		function array(arr, stack) {
			var i = arr.length,
				ret = new Array(i);

			if (dump.maxDepth && dump.depth > dump.maxDepth) {
				return "[object Array]";
			}

			this.up();
			while (i--) {
				ret[i] = this.parse(arr[i], undefined, stack);
			}
			this.down();
			return join("[", ret, "]");
		}

		function isArray(obj) {
			return (

				//Native Arrays
				toString.call(obj) === "[object Array]" ||

				// NodeList objects
				(typeof obj.length === "number" && obj.item !== undefined) &&
				(obj.length ?
					obj.item(0) === obj[0] :
					(obj.item(0) === null && obj[0] === undefined)
				)
			);
		}

		var reName = /^function (\w+)/,
			dump = {

				// The objType is used mostly internally, you can fix a (custom) type in advance
				parse: function (obj, objType, stack) {
					stack = stack || [];
					var res, parser, parserType,
						inStack = inArray(obj, stack);

					if (inStack !== -1) {
						return "recursion(" + (inStack - stack.length) + ")";
					}

					objType = objType || this.typeOf(obj);
					parser = this.parsers[objType];
					parserType = typeof parser;

					if (parserType === "function") {
						stack.push(obj);
						res = parser.call(this, obj, stack);
						stack.pop();
						return res;
					}
					return (parserType === "string") ? parser : this.parsers.error;
				},
				typeOf: function (obj) {
					var type;

					if (obj === null) {
						type = "null";
					} else if (typeof obj === "undefined") {
						type = "undefined";
					} else if (QUnit.is("regexp", obj)) {
						type = "regexp";
					} else if (QUnit.is("date", obj)) {
						type = "date";
					} else if (QUnit.is("function", obj)) {
						type = "function";
					} else if (obj.setInterval !== undefined &&
						obj.document !== undefined &&
						obj.nodeType === undefined) {
						type = "window";
					} else if (obj.nodeType === 9) {
						type = "document";
					} else if (obj.nodeType) {
						type = "node";
					} else if (isArray(obj)) {
						type = "array";
					} else if (obj.constructor === Error.prototype.constructor) {
						type = "error";
					} else {
						type = typeof obj;
					}
					return type;
				},

				separator: function () {
					return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; }, // Extra can be a number, shortcut for increasing-calling-decreasing indent: function (extra) { if (!this.multiline) { return ""; } var chr = this.indentChar; if (this.HTML) { chr = chr.replace(/\t/g, " ").replace(/ /g, " "); } return new Array(this.depth + (extra || 0)).join(chr); }, up: function (a) { this.depth += a || 1; }, down: function (a) { this.depth -= a || 1; }, setParser: function (name, parser) { this.parsers[name] = parser; }, // The next 3 are exposed so you can use them quote: quote, literal: literal, join: join, depth: 1, maxDepth: QUnit.config.maxDepth, // This is the list of parsers, to modify them, use dump.setParser parsers: { window: "[Window]", document: "[Document]", error: function (error) { return "Error(\"" + error.message + "\")"; }, unknown: "[Unknown]", "null": "null", "undefined": "undefined", "function": function (fn) { var ret = "function", // Functions never have name in IE name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; if (name) { ret += " " + name; } ret += "("; ret = [ret, dump.parse(fn, "functionArgs"), "){"].join(""); return join(ret, dump.parse(fn, "functionCode"), "}"); }, array: array, nodelist: array, "arguments": array, object: function (map, stack) { var keys, key, val, i, nonEnumerableProperties, ret = []; if (dump.maxDepth && dump.depth > dump.maxDepth) { return "[object Object]"; } dump.up(); keys = []; for (key in map) { keys.push(key); } // Some properties are not always enumerable on Error objects. nonEnumerableProperties = ["message", "name"]; for (i in nonEnumerableProperties) { key = nonEnumerableProperties[i]; if (key in map && inArray(key, keys) < 0) { keys.push(key); } } keys.sort(); for (i = 0; i < keys.length; i++) { key = keys[i]; val = map[key]; ret.push(dump.parse(key, "key") + ": " + dump.parse(val, undefined, stack)); } dump.down(); return join("{", ret, "}"); }, node: function (node) { var len, i, val, open = dump.HTML ? "<" : "<", close = dump.HTML ? ">" : ">", tag = node.nodeName.toLowerCase(), ret = open + tag, attrs = node.attributes; if (attrs) { for (i = 0, len = attrs.length; i < len; i++) { val = attrs[i].nodeValue; // IE6 includes all attributes in .attributes, even ones not explicitly // set. Those have values like undefined, null, 0, false, "" or // "inherit". if (val && val !== "inherit") { ret += " " + attrs[i].nodeName + "=" + dump.parse(val, "attribute"); } } } ret += close; // Show content of TextNode or CDATASection if (node.nodeType === 3 || node.nodeType === 4) { ret += node.nodeValue; } return ret + open + "/" + tag + close; }, // Function calls it internally, it's the arguments part of the function functionArgs: function (fn) { var args, l = fn.length; if (!l) { return ""; } args = new Array(l); while (l--) { // 97 is 'a' args[l] = String.fromCharCode(97 + l); } return " " + args.join(", ") + " "; }, // Object calls it internally, the key part of an item in a map key: quote, // Function calls it internally, it's the content of the function functionCode: "[code]", // Node calls it internally, it's a html attribute value attribute: quote, string: quote, date: quote, regexp: literal, number: literal, "boolean": literal, symbol: function (sym) { return sym.toString(); } }, // If true, entities are escaped ( <, >, \t, space and \n ) HTML: false, // Indentation unit indentChar: " ", // If true, items in a collection, are separated by a \n, else just a space. multiline: true }; return dump; }()); // Back compat QUnit.jsDump = QUnit.dump; function applyDeprecated(name) { return function () { throw new Error( name + " is removed in QUnit 2.0.\n" + "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" ); }; } Object.keys(Assert.prototype).forEach(function (key) { QUnit[key] = applyDeprecated("`QUnit." + key + "`"); }); QUnit.asyncTest = function () { throw new Error( "asyncTest is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" ); }; QUnit.stop = function () { throw new Error( "QUnit.stop is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" ); }; function resetThrower() { throw new Error( "QUnit.reset is removed in QUnit 2.0 without replacement.\n" + "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" ); } Object.defineProperty(QUnit, "reset", { get: function () { return resetThrower; }, set: resetThrower }); if (defined.document) { if (window.QUnit) { throw new Error("QUnit has already been defined."); } [ "test", "module", "expect", "start", "ok", "notOk", "equal", "notEqual", "propEqual", "notPropEqual", "deepEqual", "notDeepEqual", "strictEqual", "notStrictEqual", "throws", "raises" ].forEach(function (key) { window[key] = applyDeprecated("The global `" + key + "`"); }); window.QUnit = QUnit; } // For nodejs if (typeof module !== "undefined" && module && module.exports) { module.exports = QUnit; // For consistency with CommonJS environments' exports module.exports.QUnit = QUnit; } // For CommonJS with exports, but without module.exports, like Rhino if (typeof exports !== "undefined" && exports) { exports.QUnit = QUnit; } if (typeof define === "function" && define.amd) { define(function () { return QUnit; }); QUnit.config.autostart = false; } // Get a reference to the global object, like window in browsers }((function () { return this; }()))); (function () { if (typeof window === "undefined" || !window.document) { return; } var config = QUnit.config, hasOwn = Object.prototype.hasOwnProperty; // Stores fixture HTML for resetting later function storeFixture() { // Avoid overwriting user-defined values if (hasOwn.call(config, "fixture")) { return; } var fixture = document.getElementById("qunit-fixture"); if (fixture) { config.fixture = fixture.innerHTML; } } QUnit.begin(storeFixture); // Resets the fixture DOM element if available. function resetFixture() { if (config.fixture == null) { return; } var fixture = document.getElementById("qunit-fixture"); if (fixture) { fixture.innerHTML = config.fixture; } } QUnit.testStart(resetFixture); }()); (function () { // Only interact with URLs via window.location var location = typeof window !== "undefined" && window.location; if (!location) { return; } var urlParams = getUrlParams(); QUnit.urlParams = urlParams; // Match module/test by inclusion in an array QUnit.config.moduleId = [].concat(urlParams.moduleId || []); QUnit.config.testId = [].concat(urlParams.testId || []); // Exact case-insensitive match of the module name QUnit.config.module = urlParams.module; // Regular expression or case-insenstive substring match against "moduleName: testName" QUnit.config.filter = urlParams.filter; // Test order randomization if (urlParams.seed === true) { // Generate a random seed if the option is specified without a value QUnit.config.seed = Math.random().toString(36).slice(2); } else if (urlParams.seed) { QUnit.config.seed = urlParams.seed; } // Add URL-parameter-mapped config values with UI form rendering data QUnit.config.urlConfig.push( { id: "hidepassed", label: "Hide passed tests", tooltip: "Only show tests and assertions that fail. Storedquery-strings." }, { id: "noglobals", label: "Check for Globals", tooltip: "Enabling this will test if any test introduces new properties on the " + "global object (`window` in Browsers). Storedquery-strings." }, { id: "notrycatch", label: "No try-catch", tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + "exceptions in IE reasonable. Storedquery-strings." } ); QUnit.begin(function () { var i, option, urlConfig = QUnit.config.urlConfig; for (i = 0; i < urlConfig.length; i++) { // Options can be either strings or objects with nonempty "id" properties option = QUnit.config.urlConfig[i]; if (typeof option !== "string") { option = option.id; } if (QUnit.config[option] === undefined) { QUnit.config[option] = urlParams[option]; } } }); function getUrlParams() { var i, param, name, value; var urlParams = {}; var params = location.search.slice(1).split("&"); var length = params.length; for (i = 0; i < length; i++) { if (params[i]) { param = params[i].split("="); name = decodeQueryParam(param[0]); // Allow just a key to turn on a flag, e.g., test.html?noglobals value = param.length === 1 || decodeQueryParam(param.slice(1).join("=")); if (urlParams[name]) { urlParams[name] = [].concat(urlParams[name], value); } else { urlParams[name] = value; } } } return urlParams; } function decodeQueryParam(param) { return decodeURIComponent(param.replace(/\+/g, "%20")); } // Don't load the HTML Reporter on non-browser environments if (typeof window === "undefined" || !window.document) { return; } QUnit.init = function () { throw new Error( "QUnit.init is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" ); }; var config = QUnit.config, document = window.document, collapseNext = false, hasOwn = Object.prototype.hasOwnProperty, unfilteredUrl = setUrl({ filter: undefined, module: undefined, moduleId: undefined, testId: undefined }), defined = { sessionStorage: (function () { var x = "qunit-test-string"; try { sessionStorage.setItem(x, x); sessionStorage.removeItem(x); return true; } catch (e) { return false; } }()) }, modulesList = []; // Escape text for attribute or text content. function escapeText(s) { if (!s) { return ""; } s = s + ""; // Both single quotes and double quotes (for attributes) return s.replace(/['"<>&]/g, function (s) { switch (s) { case "'": return "'"; case "\"": return """; case "<": return "<"; case ">": return ">"; case "&": return "&"; } }); } function addEvent(elem, type, fn) { elem.addEventListener(type, fn, false); } function removeEvent(elem, type, fn) { elem.removeEventListener(type, fn, false); } function addEvents(elems, type, fn) { var i = elems.length; while (i--) { addEvent(elems[i], type, fn); } } function hasClass(elem, name) { return (" " + elem.className + " ").indexOf(" " + name + " ") >= 0; } function addClass(elem, name) { if (!hasClass(elem, name)) { elem.className += (elem.className ? " " : "") + name; } } function toggleClass(elem, name, force) { if (force || typeof force === "undefined" && !hasClass(elem, name)) { addClass(elem, name); } else { removeClass(elem, name); } } function removeClass(elem, name) { var set = " " + elem.className + " "; // Class name may appear multiple times while (set.indexOf(" " + name + " ") >= 0) { set = set.replace(" " + name + " ", " "); } // Trim for prettiness elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); } function id(name) { return document.getElementById && document.getElementById(name); } function interceptNavigation(ev) { applyUrlParams(); if (ev && ev.preventDefault) { ev.preventDefault(); } return false; } function getUrlConfigHtml() { var i, j, val, escaped, escapedTooltip, selection = false, urlConfig = config.urlConfig, urlConfigHtml = ""; for (i = 0; i < urlConfig.length; i++) { // Options can be either strings or objects with nonempty "id" properties val = config.urlConfig[i]; if (typeof val === "string") { val = { id: val, label: val }; } escaped = escapeText(val.id); escapedTooltip = escapeText(val.tooltip); if (!val.value || typeof val.value === "string") { urlConfigHtml += ""; } else { urlConfigHtml += ""; } } return urlConfigHtml; } // Handle "click" events on toolbar checkboxes and "change" for select menus. // Updates the URL with the new state of `config.urlConfig` values. function toolbarChanged() { var updatedUrl, value, tests, field = this, params = {}; // Detect if field is a select menu or a checkbox if ("selectedIndex" in field) { value = field.options[field.selectedIndex].value || undefined; } else { value = field.checked ? (field.defaultValue || true) : undefined; } params[field.name] = value; updatedUrl = setUrl(params); // Check if we can apply the change without a page refresh if ("hidepassed" === field.name && "replaceState" in window.history) { QUnit.urlParams[field.name] = value; config[field.name] = value || false; tests = id("qunit-tests"); if (tests) { toggleClass(tests, "hidepass", value || false); } window.history.replaceState(null, "", updatedUrl); } else { window.location = updatedUrl; } } function setUrl(params) { var key, arrValue, i, querystring = "?", location = window.location; params = QUnit.extend(QUnit.extend({}, QUnit.urlParams), params); for (key in params) { // Skip inherited or undefined properties if (hasOwn.call(params, key) && params[key] !== undefined) { // Output a parameter for each value of this key (but usually just one) arrValue = [].concat(params[key]); for (i = 0; i < arrValue.length; i++) { querystring += encodeURIComponent(key); if (arrValue[i] !== true) { querystring += "=" + encodeURIComponent(arrValue[i]); } querystring += "&"; } } } return location.protocol + "//" + location.host + location.pathname + querystring.slice(0, -1); } function applyUrlParams() { var i, selectedModules = [], modulesList = id("qunit-modulefilter-dropdown-list").getElementsByTagName("input"), filter = id("qunit-filter-input").value; for (i = 0; i < modulesList.length; i++) { if (modulesList[i].checked) { selectedModules.push(modulesList[i].value); } } window.location = setUrl({ filter: (filter === "") ? undefined : filter, moduleId: (selectedModules.length === 0) ? undefined : selectedModules, // Remove module and testId filter module: undefined, testId: undefined }); } function toolbarUrlConfigContainer() { var urlConfigContainer = document.createElement("span"); urlConfigContainer.innerHTML = getUrlConfigHtml(); addClass(urlConfigContainer, "qunit-url-config"); addEvents(urlConfigContainer.getElementsByTagName("input"), "change", toolbarChanged); addEvents(urlConfigContainer.getElementsByTagName("select"), "change", toolbarChanged); return urlConfigContainer; } function toolbarLooseFilter() { var filter = document.createElement("form"), label = document.createElement("label"), input = document.createElement("input"), button = document.createElement("button"); addClass(filter, "qunit-filter"); label.innerHTML = "Filter: "; input.type = "text"; input.value = config.filter || ""; input.name = "filter"; input.id = "qunit-filter-input"; button.innerHTML = "Go"; label.appendChild(input); filter.appendChild(label); filter.appendChild(document.createTextNode(" ")); filter.appendChild(button); addEvent(filter, "submit", interceptNavigation); return filter; } function moduleListHtml() { var i, checked, html = ""; for (i = 0; i < config.modules.length; i++) { if (config.modules[i].name !== "") { checked = config.moduleId.indexOf(config.modules[i].moduleId) > -1; html += "
  • "; } } return html; } function toolbarModuleFilter() { var allCheckbox, commit, reset, moduleFilter = document.createElement("form"), label = document.createElement("label"), moduleSearch = document.createElement("input"), dropDown = document.createElement("div"), actions = document.createElement("span"), dropDownList = document.createElement("ul"), dirty = false; moduleSearch.id = "qunit-modulefilter-search"; addEvent(moduleSearch, "input", searchInput); addEvent(moduleSearch, "input", searchFocus); addEvent(moduleSearch, "focus", searchFocus); addEvent(moduleSearch, "click", searchFocus); label.id = "qunit-modulefilter-search-container"; label.innerHTML = "Module: "; label.appendChild(moduleSearch); actions.id = "qunit-modulefilter-actions"; actions.innerHTML = "" + "" + ""; allCheckbox = actions.lastChild.firstChild; commit = actions.firstChild; reset = commit.nextSibling; addEvent(commit, "click", applyUrlParams); dropDownList.id = "qunit-modulefilter-dropdown-list"; dropDownList.innerHTML = moduleListHtml(); dropDown.id = "qunit-modulefilter-dropdown"; dropDown.style.display = "none"; dropDown.appendChild(actions); dropDown.appendChild(dropDownList); addEvent(dropDown, "change", selectionChange); selectionChange(); moduleFilter.id = "qunit-modulefilter"; moduleFilter.appendChild(label); moduleFilter.appendChild(dropDown); addEvent(moduleFilter, "submit", interceptNavigation); addEvent(moduleFilter, "reset", function () { // Let the reset happen, then update styles window.setTimeout(selectionChange); }); // Enables show/hide for the dropdown function searchFocus() { if (dropDown.style.display !== "none") { return; } dropDown.style.display = "block"; addEvent(document, "click", hideHandler); addEvent(document, "keydown", hideHandler); // Hide on Escape keydown or outside-container click function hideHandler(e) { var inContainer = moduleFilter.contains(e.target); if (e.keyCode === 27 || !inContainer) { if (e.keyCode === 27 && inContainer) { moduleSearch.focus(); } dropDown.style.display = "none"; removeEvent(document, "click", hideHandler); removeEvent(document, "keydown", hideHandler); moduleSearch.value = ""; searchInput(); } } } // Processes module search box input function searchInput() { var i, item, searchText = moduleSearch.value.toLowerCase(), listItems = dropDownList.children; for (i = 0; i < listItems.length; i++) { item = listItems[i]; if (!searchText || item.textContent.toLowerCase().indexOf(searchText) > -1) { item.style.display = ""; } else { item.style.display = "none"; } } } // Processes selection changes function selectionChange(evt) { var i, item, checkbox = evt && evt.target || allCheckbox, modulesList = dropDownList.getElementsByTagName("input"), selectedNames = []; toggleClass(checkbox.parentNode, "checked", checkbox.checked); dirty = false; if (checkbox.checked && checkbox !== allCheckbox) { allCheckbox.checked = false; removeClass(allCheckbox.parentNode, "checked"); } for (i = 0; i < modulesList.length; i++) { item = modulesList[i]; if (!evt) { toggleClass(item.parentNode, "checked", item.checked); } else if (checkbox === allCheckbox && checkbox.checked) { item.checked = false; removeClass(item.parentNode, "checked"); } dirty = dirty || (item.checked !== item.defaultChecked); if (item.checked) { selectedNames.push(item.parentNode.textContent); } } commit.style.display = reset.style.display = dirty ? "" : "none"; moduleSearch.placeholder = selectedNames.join(", ") || allCheckbox.parentNode.textContent; moduleSearch.title = "Type to filter list. Current selection:\n" + (selectedNames.join("\n") || allCheckbox.parentNode.textContent); } return moduleFilter; } function appendToolbar() { var toolbar = id("qunit-testrunner-toolbar"); if (toolbar) { toolbar.appendChild(toolbarUrlConfigContainer()); toolbar.appendChild(toolbarModuleFilter()); toolbar.appendChild(toolbarLooseFilter()); toolbar.appendChild(document.createElement("div")).className = "clearfix"; } } function appendHeader() { var header = id("qunit-header"); if (header) { header.innerHTML = "" + header.innerHTML + " "; } } function appendBanner() { var banner = id("qunit-banner"); if (banner) { banner.className = ""; } } function appendTestResults() { var tests = id("qunit-tests"), result = id("qunit-testresult"); if (result) { result.parentNode.removeChild(result); } if (tests) { tests.innerHTML = ""; result = document.createElement("p"); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore(result, tests); result.innerHTML = "Running...
     "; } } function appendFilteredTest() { var testId = QUnit.config.testId; if (!testId || testId.length <= 0) { return ""; } return "
    Rerunning selected tests: " + escapeText(testId.join(", ")) + " Run all tests
    "; } function appendUserAgent() { var userAgent = id("qunit-userAgent"); if (userAgent) { userAgent.innerHTML = ""; userAgent.appendChild( document.createTextNode( "QUnit " + QUnit.version + "; " + navigator.userAgent ) ); } } function appendInterface() { var qunit = id("qunit"); if (qunit) { qunit.innerHTML = "

    " + escapeText(document.title) + "

    " + "

    " + "
    " + appendFilteredTest() + "

    " + "
      "; } appendHeader(); appendBanner(); appendTestResults(); appendUserAgent(); appendToolbar(); } function appendTestsList(modules) { var i, l, x, z, test, moduleObj; for (i = 0, l = modules.length; i < l; i++) { moduleObj = modules[i]; for (x = 0, z = moduleObj.tests.length; x < z; x++) { test = moduleObj.tests[x]; appendTest(test.name, test.testId, moduleObj.name); } } } function appendTest(name, testId, moduleName) { var title, rerunTrigger, testBlock, assertList, tests = id("qunit-tests"); if (!tests) { return; } title = document.createElement("strong"); title.innerHTML = getNameHtml(name, moduleName); rerunTrigger = document.createElement("a"); rerunTrigger.innerHTML = "Rerun"; rerunTrigger.href = setUrl({ testId: testId }); testBlock = document.createElement("li"); testBlock.appendChild(title); testBlock.appendChild(rerunTrigger); testBlock.id = "qunit-test-output-" + testId; assertList = document.createElement("ol"); assertList.className = "qunit-assert-list"; testBlock.appendChild(assertList); tests.appendChild(testBlock); } // HTML Reporter initialization and load QUnit.begin(function (details) { var i, moduleObj, tests; // Sort modules by name for the picker for (i = 0; i < details.modules.length; i++) { moduleObj = details.modules[i]; if (moduleObj.name) { modulesList.push(moduleObj.name); } } modulesList.sort(function (a, b) { return a.localeCompare(b); }); // Initialize QUnit elements appendInterface(); appendTestsList(details.modules); tests = id("qunit-tests"); if (tests && config.hidepassed) { addClass(tests, "hidepass"); } }); QUnit.done(function (details) { var i, key, banner = id("qunit-banner"), tests = id("qunit-tests"), html = [ "Tests completed in ", details.runtime, " milliseconds.
      ", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed." ].join(""); if (banner) { banner.className = details.failed ? "qunit-fail" : "qunit-pass"; } if (tests) { id("qunit-testresult").innerHTML = html; } if (config.altertitle && document.title) { // Show ? for good, ? for bad suite result in title // use escape sequences in case file gets loaded with non-utf-8-charset document.title = [ (details.failed ? "\u2716" : "\u2714"), document.title.replace(/^[\u2714\u2716] /i, "") ].join(" "); } // Clear own sessionStorage items if all tests passed if (config.reorder && defined.sessionStorage && details.failed === 0) { for (i = 0; i < sessionStorage.length; i++) { key = sessionStorage.key(i++); if (key.indexOf("qunit-test-") === 0) { sessionStorage.removeItem(key); } } } // Scroll back to top to show results if (config.scrolltop && window.scrollTo) { window.scrollTo(0, 0); } }); function getNameHtml(name, module) { var nameHtml = ""; if (module) { nameHtml = "" + escapeText(module) + ": "; } nameHtml += "" + escapeText(name) + ""; return nameHtml; } QUnit.testStart(function (details) { var running, testBlock, bad; testBlock = id("qunit-test-output-" + details.testId); if (testBlock) { testBlock.className = "running"; } else { // Report later registered tests appendTest(details.name, details.testId, details.module); } running = id("qunit-testresult"); if (running) { bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + details.module + "-" + details.name); running.innerHTML = (bad ? "Rerunning previously failed test:
      " : "Running:
      ") + getNameHtml(details.name, details.module); } }); function stripHtml(string) { // Strip tags, html entity and whitespaces return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); } QUnit.log(function (details) { var assertList, assertLi, message, expected, actual, diff, showDiff = false, testItem = id("qunit-test-output-" + details.testId); if (!testItem) { return; } message = escapeText(details.message) || (details.result ? "okay" : "failed"); message = "" + message + ""; message += "@ " + details.runtime + " ms"; // The pushFailure doesn't provide details.expected // when it calls, it's implicit to also not show expected and diff stuff // Also, we need to check details.expected existence,it can exist and be undefined if (!details.result && hasOwn.call(details, "expected")) { if (details.negative) { expected = "NOT " + QUnit.dump.parse(details.expected); } else { expected = QUnit.dump.parse(details.expected); } actual = QUnit.dump.parse(details.actual); message += ""; if (actual !== expected) { message += ""; // Don't show diff if actual or expected are booleans if (!(/^(true|false)$/.test(actual)) && !(/^(true|false)$/.test(expected))) { diff = QUnit.diff(expected, actual); showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; } // Don't show diff if expected and actual are totally different if (showDiff) { message += ""; } } else if (expected.indexOf("[object Array]") !== -1 || expected.indexOf("[object Object]") !== -1) { message += ""; } else { message += ""; } if (details.source) { message += ""; } message += "
      Expected:
      " +
      				escapeText(expected) +
      				"
      Result:
      " +
      					escapeText(actual) + "
      Diff:
      " +
      						diff + "
      Message: " + "Diff suppressedthe depth of object is more than current max depth (" + QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + " run with a higher max depth or " + "Rerun without max depth.

      Message: " + "Diff suppressedthe expected and actual results have an equivalent" + " serialization
      Source:
      " +
      					escapeText(details.source) + "
      "; // This occurs when pushFailure is set and we have an extracted stack trace } else if (!details.result && details.source) { message += "" + "" + "
      Source:
      " +
      				escapeText(details.source) + "
      "; } assertList = testItem.getElementsByTagName("ol")[0]; assertLi = document.createElement("li"); assertLi.className = details.result ? "pass" : "fail"; assertLi.innerHTML = message; assertList.appendChild(assertLi); }); QUnit.testDone(function (details) { var testTitle, time, testItem, assertList, good, bad, testCounts, skipped, sourceName, tests = id("qunit-tests"); if (!tests) { return; } testItem = id("qunit-test-output-" + details.testId); assertList = testItem.getElementsByTagName("ol")[0]; good = details.passed; bad = details.failed; // Store result when possible if (config.reorder && defined.sessionStorage) { if (bad) { sessionStorage.setItem("qunit-test-" + details.module + "-" + details.name, bad); } else { sessionStorage.removeItem("qunit-test-" + details.module + "-" + details.name); } } if (bad === 0) { // Collapse the passing tests addClass(assertList, "qunit-collapsed"); } else if (bad && config.collapse && !collapseNext) { // Skip collapsing the first failing test collapseNext = true; } else { // Collapse remaining tests addClass(assertList, "qunit-collapsed"); } // The testItem.firstChild is the test name testTitle = testItem.firstChild; testCounts = bad ? "" + bad + ", " + "" + good + ", " : ""; testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; if (details.skipped) { testItem.className = "skipped"; skipped = document.createElement("em"); skipped.className = "qunit-skipped-label"; skipped.innerHTML = "skipped"; testItem.insertBefore(skipped, testTitle); } else { addEvent(testTitle, "click", function () { toggleClass(assertList, "qunit-collapsed"); }); testItem.className = bad ? "fail" : "pass"; time = document.createElement("span"); time.className = "runtime"; time.innerHTML = details.runtime + " ms"; testItem.insertBefore(time, assertList); } // Show the source of the test when showing assertions if (details.source) { sourceName = document.createElement("p"); sourceName.innerHTML = "Source: " + details.source; addClass(sourceName, "qunit-source"); if (bad === 0) { addClass(sourceName, "qunit-collapsed"); } addEvent(testTitle, "click", function () { toggleClass(sourceName, "qunit-collapsed"); }); testItem.appendChild(sourceName); } }); // Avoid readyState issue with phantomjs // Ref: #818 var notPhantom = (function (p) { return !(p && p.version && p.version.major > 0); })(window.phantom); if (notPhantom && document.readyState === "complete") { QUnit.load(); } else { addEvent(window, "load", QUnit.load); } /* * This file is a modified version of google-diff-match-patch's JavaScript implementation * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), * modifications are licensedmore fully set forth in LICENSE.txt. * * The original source of google-diff-match-patch is attributable and licensedfollows: * * Copyright 2006 Google Inc. * https://code.google.com/p/google-diff-match-patch/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * More Info: * https://code.google.com/p/google-diff-match-patch/ * * Usage: QUnit.diff(expected, actual) * */ QUnit.diff = (function () { function DiffMatchPatch() { } // DIFF FUNCTIONS /** * The data structure representing a diff is an array of tuples: * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] * which means: delete 'Hello', add 'Goodbye' and keep ' world.' */ var DIFF_DELETE = -1, DIFF_INSERT = 1, DIFF_EQUAL = 0; /** * Find the differences between two texts. Simplifies the problem by stripping * any common prefix or suffix off the texts before diffing. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean=} optChecklines Optional speedup flag. If present and false, * then don't run a line-level diff first to identify the changed areas. * Defaults to true, which does a faster, slightly less optimal diff. * @return {!Array.} Array of diff tuples. */ DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; // The diff must be complete in up to 1 second. deadline = (new Date()).getTime() + 1000; // Check for null inputs. if (text1 === null || text2 === null) { throw new Error("Null input. (DiffMain)"); } // Check for equality (speedup). if (text1 === text2) { if (text1) { return [ [DIFF_EQUAL, text1] ]; } return []; } if (typeof optChecklines === "undefined") { optChecklines = true; } checklines = optChecklines; // Trim off common prefix (speedup). commonlength = this.diffCommonPrefix(text1, text2); commonprefix = text1.substring(0, commonlength); text1 = text1.substring(commonlength); text2 = text2.substring(commonlength); // Trim off common suffix (speedup). commonlength = this.diffCommonSuffix(text1, text2); commonsuffix = text1.substring(text1.length - commonlength); text1 = text1.substring(0, text1.length - commonlength); text2 = text2.substring(0, text2.length - commonlength); // Compute the diff on the middle block. diffs = this.diffCompute(text1, text2, checklines, deadline); // Restore the prefix and suffix. if (commonprefix) { diffs.unshift([DIFF_EQUAL, commonprefix]); } if (commonsuffix) { diffs.push([DIFF_EQUAL, commonsuffix]); } this.diffCleanupMerge(diffs); return diffs; }; /** * Reduce the number of edits by eliminating operationally trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Is there an insertion operation before the last equality. preIns = false; // Is there a deletion operation before the last equality. preDel = false; // Is there an insertion operation after the last equality. postIns = false; // Is there a deletion operation after the last equality. postDel = false; while (pointer < diffs.length) { // Equality found. if (diffs[pointer][0] === DIFF_EQUAL) { if (diffs[pointer][1].length < 4 && (postIns || postDel)) { // Candidate found. equalities[equalitiesLength++] = pointer; preIns = postIns; preDel = postDel; lastequality = diffs[pointer][1]; } else { // Not a candidate, and can never become one. equalitiesLength = 0; lastequality = null; } postIns = postDel = false; // An insertion or deletion. } else { if (diffs[pointer][0] === DIFF_DELETE) { postDel = true; } else { postIns = true; } /* * Five types to be split: * ABXYCD * AXCD * ABXC * AXCD * ABXC */ if (lastequality && ((preIns && preDel && postIns && postDel) || ((lastequality.length < 2) && (preIns + preDel + postIns + postDel) === 3))) { // Duplicate record. diffs.splice( equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality] ); // Change second copy to insert. diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; equalitiesLength--; // Throw away the equality we just deleted; lastequality = null; if (preIns && preDel) { // No changes made which could affect previous entry, keep going. postIns = postDel = true; equalitiesLength = 0; } else { equalitiesLength--; // Throw away the previous equality. pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; postIns = postDel = false; } changes = true; } } pointer++; } if (changes) { this.diffCleanupMerge(diffs); } }; /** * Convert a diff array into a pretty HTML report. * @param {!Array.} diffs Array of diff tuples. * @param {integer} string to be beautified. * @return {string} HTML representation. */ DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { var op, data, x, html = []; for (x = 0; x < diffs.length; x++) { op = diffs[x][0]; // Operation (insert, delete, equal) data = diffs[x][1]; // Text of change. switch (op) { case DIFF_INSERT: html[x] = "" + escapeText(data) + ""; break; case DIFF_DELETE: html[x] = "" + escapeText(data) + ""; break; case DIFF_EQUAL: html[x] = "" + escapeText(data) + ""; break; } } return html.join(""); }; /** * Determine the common prefix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the start of each * string. */ DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { var pointermid, pointermax, pointermin, pointerstart; // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min(text1.length, text2.length); pointermid = pointermax; pointerstart = 0; while (pointermin < pointermid) { if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { pointermin = pointermid; pointerstart = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Determine the common suffix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of each string. */ DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { var pointermid, pointermax, pointermin, pointerend; // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min(text1.length, text2.length); pointermid = pointermax; pointerend = 0; while (pointermin < pointermid) { if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { pointermin = pointermid; pointerend = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Find the differences between two texts. Assumes that the texts do not * have any common prefix or suffix. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean} checklines Speedup flag. If false, then don't run a * line-level diff first to identify the changed areas. * If true, then run a faster, slightly less optimal diff. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; if (!text1) { // Just add some text (speedup). return [ [DIFF_INSERT, text2] ]; } if (!text2) { // Just delete some text (speedup). return [ [DIFF_DELETE, text1] ]; } longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; i = longtext.indexOf(shorttext); if (i !== -1) { // Shorter text is inside the longer text (speedup). diffs = [ [DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)] ]; // Swap insertions for deletions if diff is reversed. if (text1.length > text2.length) { diffs[0][0] = diffs[2][0] = DIFF_DELETE; } return diffs; } if (shorttext.length === 1) { // Single character string. // After the previous speedup, the character can't be an equality. return [ [DIFF_DELETE, text1], [DIFF_INSERT, text2] ]; } // Check to see if the problem can be split in two. hm = this.diffHalfMatch(text1, text2); if (hm) { // A half-match was found, sort out the return data. text1A = hm[0]; text1B = hm[1]; text2A = hm[2]; text2B = hm[3]; midCommon = hm[4]; // Send both pairs off for separate processing. diffsA = this.DiffMain(text1A, text2A, checklines, deadline); diffsB = this.DiffMain(text1B, text2B, checklines, deadline); // Merge the results. return diffsA.concat([ [DIFF_EQUAL, midCommon] ], diffsB); } if (checklines && text1.length > 100 && text2.length > 100) { return this.diffLineMode(text1, text2, deadline); } return this.diffBisect(text1, text2, deadline); }; /** * Do the two texts share a substring which is at least half the length of the * longer text? * This speedup can produce non-minimal diffs. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {Array.} Five element Array, containing the prefix of * text1, the suffix of text1, the prefix of text2, the suffix of * text2 and the common middle. Or null if there was no match. * @private */ DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { return null; // Pointless. } dmp = this; // 'this' becomes 'window' in a closure. /** * Does a substring of shorttext exist within longtext such that the substring * is at least half the length of longtext? * Closure, but does not reference any external variables. * @param {string} longtext Longer string. * @param {string} shorttext Shorter string. * @param {number} i Start index of quarter length substring within longtext. * @return {Array.} Five element Array, containing the prefix of * longtext, the suffix of longtext, the prefix of shorttext, the suffix * of shorttext and the common middle. Or null if there was no match. * @private */ function diffHalfMatchI(longtext, shorttext, i) { var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; // Start with a 1/4 length substring at position ia seed. seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); j = -1; bestCommon = ""; while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); if (bestCommon.length < suffixLength + prefixLength) { bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); bestLongtextA = longtext.substring(0, i - suffixLength); bestLongtextB = longtext.substring(i + prefixLength); bestShorttextA = shorttext.substring(0, j - suffixLength); bestShorttextB = shorttext.substring(j + prefixLength); } } if (bestCommon.length * 2 >= longtext.length) { return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon ]; } else { return null; } } // First check if the second quarter is the seed for a half-match. hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); // Check again based on the third quarter. hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); if (!hm1 && !hm2) { return null; } else if (!hm2) { hm = hm1; } else if (!hm1) { hm = hm2; } else { // Both matched. Select the longest. hm = hm1[4].length > hm2[4].length ? hm1 : hm2; } // A half-match was found, sort out the return data. text1A, text1B, text2A, text2B; if (text1.length > text2.length) { text1A = hm[0]; text1B = hm[1]; text2A = hm[2]; text2B = hm[3]; } else { text2A = hm[0]; text2B = hm[1]; text1A = hm[2]; text1B = hm[3]; } midCommon = hm[4]; return [text1A, text1B, text2A, text2B, midCommon]; }; /** * Do a quick line-level diff on both strings, then rediff the parts for * greater accuracy. * This speedup can produce non-minimal diffs. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; // Scan the text on a line-by-line basis first. a = this.diffLinesToChars(text1, text2); text1 = a.chars1; text2 = a.chars2; linearray = a.lineArray; diffs = this.DiffMain(text1, text2, false, deadline); // Convert the diff back to original text. this.diffCharsToLines(diffs, linearray); // Eliminate freak matches (e.g. blank lines) this.diffCleanupSemantic(diffs); // Rediff any replacement blocks, this time character-by-character. // Add a dummy entry at the end. diffs.push([DIFF_EQUAL, ""]); pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: countInsert++; textInsert += diffs[pointer][1]; break; case DIFF_DELETE: countDelete++; textDelete += diffs[pointer][1]; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if (countDelete >= 1 && countInsert >= 1) { // Delete the offending records and add the merged ones. diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); pointer = pointer - countDelete - countInsert; a = this.DiffMain(textDelete, textInsert, false, deadline); for (j = a.length - 1; j >= 0; j--) { diffs.splice(pointer, 0, a[j]); } pointer = pointer + a.length; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } pointer++; } diffs.pop(); // Remove the dummy entry at the end. return diffs; }; /** * Find the 'middle snake' of a diff, split the problem in two * and return the recursively constructed diff. * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; maxD = Math.ceil((text1Length + text2Length) / 2); vOffset = maxD; vLength = 2 * maxD; v1 = new Array(vLength); v2 = new Array(vLength); // Setting all elements to -1 is faster in Chrome & Firefox than mixing // integers and undefined. for (x = 0; x < vLength; x++) { v1[x] = -1; v2[x] = -1; } v1[vOffset + 1] = 0; v2[vOffset + 1] = 0; delta = text1Length - text2Length; // If the total number of characters is odd, then the front path will collide // with the reverse path. front = (delta % 2 !== 0); // Offsets for start and end of k loop. // Prevents mapping of space beyond the grid. k1start = 0; k1end = 0; k2start = 0; k2end = 0; for (d = 0; d < maxD; d++) { // Bail out if deadline is reached. if ((new Date()).getTime() > deadline) { break; } // Walk the front path one step. for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { k1Offset = vOffset + k1; if (k1 === -d || (k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1])) { x1 = v1[k1Offset + 1]; } else { x1 = v1[k1Offset - 1] + 1; } y1 = x1 - k1; while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { x1++; y1++; } v1[k1Offset] = x1; if (x1 > text1Length) { // Ran off the right of the graph. k1end += 2; } else if (y1 > text2Length) { // Ran off the bottom of the graph. k1start += 2; } else if (front) { k2Offset = vOffset + delta - k1; if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { // Mirror x2 onto top-left coordinate system. x2 = text1Length - v2[k2Offset]; if (x1 >= x2) { // Overlap detected. return this.diffBisectSplit(text1, text2, x1, y1, deadline); } } } } // Walk the reverse path one step. for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { k2Offset = vOffset + k2; if (k2 === -d || (k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1])) { x2 = v2[k2Offset + 1]; } else { x2 = v2[k2Offset - 1] + 1; } y2 = x2 - k2; while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { x2++; y2++; } v2[k2Offset] = x2; if (x2 > text1Length) { // Ran off the left of the graph. k2end += 2; } else if (y2 > text2Length) { // Ran off the top of the graph. k2start += 2; } else if (!front) { k1Offset = vOffset + delta - k2; if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { x1 = v1[k1Offset]; y1 = vOffset + x1 - k1Offset; // Mirror x2 onto top-left coordinate system. x2 = text1Length - x2; if (x1 >= x2) { // Overlap detected. return this.diffBisectSplit(text1, text2, x1, y1, deadline); } } } } } // Diff took too long and hit the deadline or // number of diffs equals number of characters, no commonality at all. return [ [DIFF_DELETE, text1], [DIFF_INSERT, text2] ]; }; /** * Given the location of the 'middle snake', split the diff in two parts * and recurse. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} x Index of split point in text1. * @param {number} y Index of split point in text2. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { var text1a, text1b, text2a, text2b, diffs, diffsb; text1a = text1.substring(0, x); text2a = text2.substring(0, y); text1b = text1.substring(x); text2b = text2.substring(y); // Compute both diffs serially. diffs = this.DiffMain(text1a, text2a, false, deadline); diffsb = this.DiffMain(text1b, text2b, false, deadline); return diffs.concat(diffsb); }; /** * Reduce the number of edits by eliminating semantically trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Number of characters that changed prior to the equality. lengthInsertions1 = 0; lengthDeletions1 = 0; // Number of characters that changed after the equality. lengthInsertions2 = 0; lengthDeletions2 = 0; while (pointer < diffs.length) { if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. equalities[equalitiesLength++] = pointer; lengthInsertions1 = lengthInsertions2; lengthDeletions1 = lengthDeletions2; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = diffs[pointer][1]; } else { // An insertion or deletion. if (diffs[pointer][0] === DIFF_INSERT) { lengthInsertions2 += diffs[pointer][1].length; } else { lengthDeletions2 += diffs[pointer][1].length; } // Eliminate an equality that is smaller or equal to the edits on both // sides of it. if (lastequality && (lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1)) && (lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2))) { // Duplicate record. diffs.splice( equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality] ); // Change second copy to insert. diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; // Throw away the equality we just deleted. equalitiesLength--; // Throw away the previous equality (it needs to be reevaluated). equalitiesLength--; pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; // Reset the counters. lengthInsertions1 = 0; lengthDeletions1 = 0; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = null; changes = true; } } pointer++; } // Normalize the diff. if (changes) { this.diffCleanupMerge(diffs); } // Find any overlaps between deletions and insertions. // e.g: abcxxxxxxdef // -> abcxxxdef // e.g: xxxabcdefxxx // -> defxxxabc // Only extract an overlap if it isbigthe edit ahead or behind it. pointer = 1; while (pointer < diffs.length) { if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { deletion = diffs[pointer - 1][1]; insertion = diffs[pointer][1]; overlapLength1 = this.diffCommonOverlap(deletion, insertion); overlapLength2 = this.diffCommonOverlap(insertion, deletion); if (overlapLength1 >= overlapLength2) { if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { // Overlap found. Insert an equality and trim the surrounding edits. diffs.splice( pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)] ); diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); diffs[pointer + 1][1] = insertion.substring(overlapLength1); pointer++; } } else { if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { // Reverse overlap found. // Insert an equality and swap and trim the surrounding edits. diffs.splice( pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)] ); diffs[pointer - 1][0] = DIFF_INSERT; diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); diffs[pointer + 1][0] = DIFF_DELETE; diffs[pointer + 1][1] = deletion.substring(overlapLength2); pointer++; } } pointer++; } pointer++; } }; /** * Determine if the suffix of one string is the prefix of another. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of the first * string and the start of the second string. * @private */ DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { var text1Length, text2Length, textLength, best, length, pattern, found; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; // Eliminate the null case. if (text1Length === 0 || text2Length === 0) { return 0; } // Truncate the longer string. if (text1Length > text2Length) { text1 = text1.substring(text1Length - text2Length); } else if (text1Length < text2Length) { text2 = text2.substring(0, text1Length); } textLength = Math.min(text1Length, text2Length); // Quick check for the worst case. if (text1 === text2) { return textLength; } // Start by looking for a single character match // and increase length until no match is found. // Performance analysis: https://neil.fraser.name/news/2010/11/04/ best = 0; length = 1; while (true) { pattern = text1.substring(textLength - length); found = text2.indexOf(pattern); if (found === -1) { return best; } length += found; if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { best = length; length++; } } }; /** * Split two texts into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {{chars1: string, chars2: string, lineArray: !Array.}} * An object containing the encoded text1, the encoded text2 and * the array of unique strings. * The zeroth element of the array of unique strings is intentionally blank. * @private */ DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { var lineArray, lineHash, chars1, chars2; lineArray = []; // E.g. lineArray[4] === 'Hello\n' lineHash = {}; // E.g. lineHash['Hello\n'] === 4 // '\x00' is a valid character, but various debuggers don't like it. // So we'll insert a junk entry to avoid generating a null character. lineArray[0] = ""; /** * Split a text into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * Modifies linearray and linehash through being a closure. * @param {string} text String to encode. * @return {string} Encoded string. * @private */ function diffLinesToCharsMunge(text) { var chars, lineStart, lineEnd, lineArrayLength, line; chars = ""; // Walk the text, pulling out a substring for each line. // text.split('\n') would would temporarily double our memory footprint. // Modifying text would create many large strings to garbage collect. lineStart = 0; lineEnd = -1; // Keeping our own length variable is faster than looking it up. lineArrayLength = lineArray.length; while (lineEnd < text.length - 1) { lineEnd = text.indexOf("\n", lineStart); if (lineEnd === -1) { lineEnd = text.length - 1; } line = text.substring(lineStart, lineEnd + 1); lineStart = lineEnd + 1; if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : (lineHash[line] !== undefined)) { chars += String.fromCharCode(lineHash[line]); } else { chars += String.fromCharCode(lineArrayLength); lineHash[line] = lineArrayLength; lineArray[lineArrayLength++] = line; } } return chars; } chars1 = diffLinesToCharsMunge(text1); chars2 = diffLinesToCharsMunge(text2); return { chars1: chars1, chars2: chars2, lineArray: lineArray }; }; /** * Rehydrate the text in a diff from a string of line hashes to real lines of * text. * @param {!Array.} diffs Array of diff tuples. * @param {!Array.} lineArray Array of unique strings. * @private */ DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { var x, chars, text, y; for (x = 0; x < diffs.length; x++) { chars = diffs[x][1]; text = []; for (y = 0; y < chars.length; y++) { text[y] = lineArray[chars.charCodeAt(y)]; } diffs[x][1] = text.join(""); } }; /** * Reorder and merge like edit sections. Merge equalities. * Any edit section can movelongit doesn't cross an equality. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { var pointer, countDelete, countInsert, textInsert, textDelete, commonlength, changes, diffPointer, position; diffs.push([DIFF_EQUAL, ""]); // Add a dummy entry at the end. pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; commonlength; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: countInsert++; textInsert += diffs[pointer][1]; pointer++; break; case DIFF_DELETE: countDelete++; textDelete += diffs[pointer][1]; pointer++; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if (countDelete + countInsert > 1) { if (countDelete !== 0 && countInsert !== 0) { // Factor out any common prefixes. commonlength = this.diffCommonPrefix(textInsert, textDelete); if (commonlength !== 0) { if ((pointer - countDelete - countInsert) > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); } else { diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength) ]); pointer++; } textInsert = textInsert.substring(commonlength); textDelete = textDelete.substring(commonlength); } // Factor out any common suffixies. commonlength = this.diffCommonSuffix(textInsert, textDelete); if (commonlength !== 0) { diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; textInsert = textInsert.substring(0, textInsert.length - commonlength); textDelete = textDelete.substring(0, textDelete.length - commonlength); } } // Delete the offending records and add the merged ones. if (countDelete === 0) { diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); } else if (countInsert === 0) { diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); } else { diffs.splice( pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert] ); } pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { // Merge this equality with the previous one. diffs[pointer - 1][1] += diffs[pointer][1]; diffs.splice(pointer, 1); } else { pointer++; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } } if (diffs[diffs.length - 1][1] === "") { diffs.pop(); // Remove the dummy entry at the end. } // Second pass: look for single edits surrounded on both sides by equalities // which can be shifted sideways to eliminate an equality. // e.g: ABAC -> ABAC changes = false; pointer = 1; // Intentionally ignore the first and last element (don't need checking). while (pointer < diffs.length - 1) { if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { diffPointer = diffs[pointer][1]; position = diffPointer.substring( diffPointer.length - diffs[pointer - 1][1].length ); // This is a single edit surrounded by equalities. if (position === diffs[pointer - 1][1]) { // Shift the edit over the previous equality. diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; diffs.splice(pointer - 1, 1); changes = true; } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { // Shift the edit over the next equality. diffs[pointer - 1][1] += diffs[pointer + 1][1]; diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; diffs.splice(pointer + 1, 1); changes = true; } } pointer++; } // If shifts were made, the diff needs reordering and another shift sweep. if (changes) { this.diffCleanupMerge(diffs); } }; return function (o, n) { var diff, output, text; diff = new DiffMatchPatch(); output = diff.DiffMain(o, n); diff.diffCleanupEfficiency(output); text = diff.diffPrettyHtml(output); return text; }; }()); }());




      © 2015 - 2025 Weber Informatics LLC | Privacy Policy