| // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. |
| // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation |
| // files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, |
| // modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the |
| // Software is furnished to do so, subject to the following conditions: |
| // |
| // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE |
| // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, |
| // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| |
| (function (window, undefined) { |
| var djstest = {}; |
| |
| window.djstest = djstest; |
| |
| djstest.indexedDB = window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.indexedDB; |
| |
| djstest.cleanStoreOnIndexedDb = function (storeObjects, done) { |
| /// <summary>Cleans all the test data saved in the IndexedDb database.</summary> |
| /// <param name="storeNames" type="Array">Array of store objects with a property that is the name of the store</param> |
| /// <param name="done" type="Function">Callback function</param> |
| |
| var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || {}; |
| |
| var deleteObjectStores = function (db) { |
| $.each(db.objectStoreNames, function (_, storeName) { |
| db.deleteObjectStore(storeName); |
| }); |
| }; |
| |
| if (djstest.indexedDB) { |
| var job = new djstest.Job(); |
| $.each(storeObjects, function (_, storeObject) { |
| job.queue((function (storeObject) { |
| return function (success, fail) { |
| var dbname = "_datajs_" + storeObject.name; |
| var request = djstest.indexedDB.open(dbname); |
| request.onsuccess = function (event) { |
| var db = request.result; |
| |
| if ("setVersion" in db) { |
| var versionRequest = db.setVersion("0.1"); |
| versionRequest.onsuccess = function (event) { |
| var transaction = versionRequest.transaction; |
| transaction.oncomplete = function () { |
| db.close(); |
| success(); |
| } |
| deleteObjectStores(db); |
| }; |
| versionRequest.onerror = function (e) { |
| djstest.fail("Error on cleanup - code: " + e.code + " name: " + e.name + "message: " + message); |
| fail(); |
| }; |
| return; |
| } |
| |
| // new api cleanup |
| db.close(); |
| var deleteRequest = djstest.indexedDB.deleteDatabase(dbname); |
| deleteRequest.onsuccess = function (event) { |
| djstest.log("djstest indexeddb cleanup - deleted database " + dbname); |
| success(); |
| }; |
| deleteRequest.onerror = function (e) { |
| djstest.fail("djstest indexeddb cleanup - error deleting database " + dbname); |
| fail(); |
| }; |
| djstest.log("djstest indexeddb cleanup - requested deletion of database " + dbname); |
| }; |
| |
| request.onerror = function (e) { |
| djstest.fail(e.code + ": " + e.message); |
| }; |
| }; |
| })(storeObject)); |
| }); |
| } |
| |
| if (job) { |
| job.run(function (succeeded) { |
| if (!succeeded) { |
| djstest.fail("cleanup job failed"); |
| } |
| done(); |
| }); |
| } |
| else { |
| done(); |
| } |
| }; |
| |
| djstest.Job = function () { |
| /// <summary>Constructs a Job object that allows for enqueuing and synchronizing the execution of functions.</summary> |
| /// <returns type="Object">Job object</returns> |
| var currentTask = -1; |
| var tasks = []; |
| |
| var failedTasks = 0; |
| |
| this.queue = function (fn) { |
| /// <summary>Adds a function to the job queue regardless if the queue is already executing or not.</summary> |
| /// <param name="fn" type="Function">Function to execute.</param> |
| tasks.push(fn); |
| }; |
| |
| this.queueNext = function (fn) { |
| /// <summary>Adds a function to the front of the job queue regardless if the queue is already executing or not.</summary> |
| /// <param name="fn" type="Function">Function to execute.</param> |
| if (currentTask < 0) { |
| tasks.unshift(fn); |
| } else { |
| tasks.splice(currentTask + 1, 0, fn); |
| } |
| }; |
| |
| this.run = function (done) { |
| /// <summary>Starts the execution of this job.</summary> |
| /// <param name="done" type="Function">Callback invoked when the job has finished executing all of its enqueued tasks.</param> |
| /// <remarks> |
| /// This method does nothing if called on a unit of work that is already executing. |
| /// </remarks> |
| |
| if (currentTask >= 0) { |
| return; |
| } |
| |
| if (tasks.length === 0) { |
| done(true); |
| return; |
| } |
| |
| var makeTaskDoneCallBack = function (failed) { |
| return function () { |
| // Track the failed task and continue the execution of the job. |
| if (failed) { |
| failedTasks++; |
| } |
| currentTask++; |
| if (currentTask === tasks.length) { |
| done(failedTasks === 0); |
| } else { |
| runNextTask(); |
| } |
| }; |
| }; |
| |
| var runNextTask = function () { |
| /// <summary>Executes the next function in the queue.</summary> |
| defer(function () { |
| try { |
| tasks[currentTask](makeTaskDoneCallBack(false), makeTaskDoneCallBack(true)); |
| } catch (e) { |
| makeTaskDoneCallBack(true)(); |
| } |
| }); |
| }; |
| |
| currentTask = 0; |
| runNextTask(); |
| }; |
| }; |
| |
| var defer = function (fn) { |
| /// <summary>Defers the execution of an arbitrary function that takes no parameters.</summary> |
| /// <param name="fn" type="Function">Function to schedule for later execution.</param> |
| setTimeout(fn, 0); |
| } |
| |
| var exposeDateValues = function (data) { |
| /// <summary>Exposes date values for Date objects to facilitate debugging</summary> |
| /// <param name="data" type="Object">The object to operate on</param> |
| if (typeof data === "object") { |
| if (data instanceof Date) { |
| data["__date__"] = data.toUTCString(); |
| } |
| else { |
| for (var prop in data) { |
| exposeDateValues(data[prop]); |
| } |
| } |
| } |
| |
| return data; |
| } |
| |
| var extractFunctionName = function (text) { |
| /// <summary>Determines the name of a function.</summary> |
| /// <param name="text" type="String">Function text.</param> |
| /// <returns type="String">The name of the function from text if found; the original text otherwise.</returns> |
| |
| var index = text.indexOf("function "); |
| if (index < 0) { |
| return text; |
| } |
| |
| var nameStart = index + "function ".length; |
| var parensIndex = text.indexOf("(", nameStart); |
| if (parensIndex < 0) { |
| return text; |
| } |
| |
| var result = text.substr(nameStart, parensIndex - nameStart); |
| if (result.indexOf("test") === 0) { |
| result = result.substr("test".length); |
| } |
| |
| return result; |
| }; |
| |
| var removeMetadata = function (data) { |
| /// <summary>Removes metadata annotations from the specified object.</summary> |
| /// <param name="data">Object to remove metadata from; possibly null.</param> |
| |
| if (typeof data === "object" && data !== null) { |
| delete data["__metadata"]; |
| for (prop in data) { |
| removeMetadata(data[prop]); |
| } |
| } |
| }; |
| |
| djstest.addFullTest = function (disable, fn, name, arg, timeout) { |
| /// <summary>Add the unit test cases</summary> |
| /// <param name="disable">Indicate whether this test case should be disabled</param> |
| if (disable != true) { |
| djstest.addTest(fn, name, arg, timeout); |
| } |
| }; |
| |
| |
| djstest.addTest = function (fn, name, arg, timeout) { |
| if (!name) { |
| name = extractFunctionName(fn.toString()); |
| } |
| |
| test(name, function () { |
| if (!timeout) { |
| timeout = 20000; |
| } |
| |
| QUnit.config.testTimeout = timeout; |
| QUnit.stop(); |
| fn.call(this, arg); |
| }); |
| }; |
| |
| djstest.assert = function (test, message) { |
| /// <summary>Asserts that a condition is true.</summary> |
| /// <param name="test" type="Boolean">Condition to test.</param> |
| /// <param name="message" type="String">Text message for condition being tested.</param> |
| QUnit.ok(test, message); |
| }; |
| |
| djstest.assertAreEqual = function (actual, expected, message) { |
| /// <summary>Asserts that the values of the expected and actualobjects are equal.</summary> |
| QUnit.equal(actual, expected, message); |
| }; |
| |
| djstest.assertAreEqualDeep = function (actual, expected, message) { |
| /// <summary>Asserts that the actual and expected objects are the same.</summary> |
| QUnit.deepEqual(exposeDateValues(actual), exposeDateValues(expected), message); |
| }; |
| |
| djstest.assertWithoutMetadata = function (actual, expected, message) { |
| removeMetadata(actual) |
| removeMetadata(expected); |
| djstest.assertAreEqualDeep(actual, expected, message); |
| }; |
| |
| djstest.asyncDo = function (asyncActions, done) { |
| /// <summary>Calls each async action in asyncActions, passing each action a function which keeps a count and |
| /// calls the passed done function when all async actions complete.</summary> |
| /// <param name="asyncActions" type="Array">Array of asynchronous actions to be executed, |
| /// each taking a single parameter - the callback function to call when the action is done.</param> |
| /// <param name="done" type="Function">Function to be executed in the last async action to complete.</param> |
| var count = 0; |
| var doneOne = function () { |
| count++; |
| if (count >= asyncActions.length) { |
| done(); |
| } |
| }; |
| |
| if (asyncActions.length > 0) { |
| $.each(asyncActions, function (_, asyncAction) { |
| asyncAction(doneOne); |
| }); |
| } else { |
| done(); |
| } |
| } |
| |
| djstest.clone = function (object) { |
| /// <summary>Makes a deep copy of an object.</summary> |
| return $.extend(true, {}, object); |
| }; |
| |
| djstest.destroyCacheAndDone = function (cache) { |
| /// <summary>Destroys the cache and then completes the test</summary> |
| /// <param name="cache">The cache to destroy</param> |
| cache.clear().then(function () { |
| djstest.done(); |
| }, function (err) { |
| djstest.fail("Failed to destroy cache: " + djstest.toString(err)); |
| djstest.done(); |
| }); |
| }; |
| |
| djstest.done = function () { |
| /// <summary>Indicates that the currently running test has finished.</summary> |
| QUnit.start(); |
| }; |
| |
| djstest.expectException = function (testFunction, message) { |
| /// <summary>Test passes if and only if an exception is thrown.</summary> |
| try { |
| testFunction(); |
| djstest.fail("Expected exception but function succeeded: " + " " + message); |
| } |
| catch (e) { |
| // Swallow exception. |
| djstest.pass("Thrown exception expected"); |
| } |
| }; |
| |
| djstest.assertsExpected = function (asserts) { |
| /// <summary>Indicates the expected number of asserts, fails test if number is not met.</summary> |
| /// <param name="asserts" type="Number">Number of asserts expected in test.</param> |
| expect(asserts); |
| } |
| |
| djstest.fail = function (message) { |
| /// <summary>Marks the current test as failed.</summary> |
| /// <param name="message" type="String">Failure message.</param> |
| QUnit.ok(false, message); |
| }; |
| |
| djstest.failAndDoneCallback = function (message, cleanupCallback) { |
| /// <summary>Returns a function that when invoked will fail this test and be done with it.</summary> |
| /// <param name="message" type="String">Failure message.</param> |
| /// <param name="cleanupCallback" type="Function" optional="true">Optional cleanup function in case of failure.</param> |
| /// <returns type="Function">A new function.</returns> |
| |
| return function (err) { |
| message = "" + message + (err) ? window.JSON.stringify(err) : ""; |
| djstest.fail(message); |
| if (cleanupCallback) { |
| try { |
| cleanupCallback(); |
| } catch (e) { |
| djstest.fail("error during cleanupCallback: " + window.JSON.stringify(e)); |
| } |
| } |
| |
| djstest.done(); |
| }; |
| }; |
| |
| djstest.log = function (message) { |
| /// <summary>Logs a test message.</summary> |
| /// <param name="message" type="String">Test message.</param> |
| var context = { result: true, actual: true, expected: true, message: message }; |
| QUnit.log(context); |
| }; |
| |
| djstest.pass = function (message) { |
| /// <summary>Marks the current test as failed.</summary> |
| /// <param name="message" type="String">Failure message.</param> |
| QUnit.ok(true, message); |
| }; |
| |
| djstest.toString = function (obj) { |
| /// <summary>Dumps the object as a string</summary> |
| /// <param name="obj" type="Object">Object to dump</param> |
| return QUnit.jsDump.parse(obj); |
| }; |
| |
| djstest.wait = function (fn) { |
| /// <summary>Executes the function, pausing test execution until the callback is called</summary> |
| /// <param name="fn" type="Function">Function to execute; takes one parameter which is the callback</param> |
| /// <remarks>This function is typically used in asynchronous setup/teardown methods</remarks> |
| QUnit.stop(); |
| fn(function () { |
| QUnit.start(); |
| }); |
| }; |
| |
| // Disable caching to ensure that every test-related AJAX request is actually being sent, |
| // and set up a default error handler |
| $.ajaxSetup({ |
| cache: false, |
| error: function (jqXHR, textStatus, errorThrown) { |
| // Work around bug in IE-Mobile on Windows Phone 7 |
| if (jqXHR.status !== 1223) { |
| var err = { |
| status: jqXHR.status, |
| statusText: jqXHR.statusText, |
| responseText: jqXHR.responseText |
| }; |
| djstest.fail("AJAX request failed with: " + djstest.toString(err)); |
| } |
| djstest.done(); |
| } |
| }); |
| })(window); |