michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "unstable" michael@0: }; michael@0: michael@0: const { Cu } = require("chrome"); michael@0: const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); michael@0: const { defer } = require("sdk/core/promise"); michael@0: const BaseAssert = require("sdk/test/assert").Assert; michael@0: const { isFunction, isObject } = require("sdk/lang/type"); michael@0: michael@0: exports.Assert = BaseAssert; michael@0: michael@0: function extend(target) { michael@0: let descriptor = {} michael@0: Array.slice(arguments, 1).forEach(function(source) { michael@0: Object.getOwnPropertyNames(source).forEach(function onEach(name) { michael@0: descriptor[name] = Object.getOwnPropertyDescriptor(source, name); michael@0: }); michael@0: }); michael@0: return Object.create(target, descriptor); michael@0: } michael@0: michael@0: /** michael@0: * Function takes test `suite` object in CommonJS format and defines all of the michael@0: * tests from that suite and nested suites in a jetpack format on a given michael@0: * `target` object. Optionally third argument `prefix` can be passed to prefix michael@0: * all the test names. michael@0: */ michael@0: function defineTestSuite(target, suite, prefix) { michael@0: prefix = prefix || ""; michael@0: // If suite defines `Assert` that's what `assert` object have to be created michael@0: // from and passed to a test function (This allows custom assertion functions) michael@0: // See for details: http://wiki.commonjs.org/wiki/Unit_Testing/1.1 michael@0: let Assert = suite.Assert || BaseAssert; michael@0: // Going through each item in the test suite and wrapping it into a michael@0: // Jetpack test format. michael@0: Object.keys(suite).forEach(function(key) { michael@0: // If name starts with test then it's a test function or suite. michael@0: if (key.indexOf("test") === 0) { michael@0: let test = suite[key]; michael@0: michael@0: // For each test function so we create a wrapper test function in a michael@0: // jetpack format and copy that to a `target` exports. michael@0: if (isFunction(test)) { michael@0: michael@0: // Since names of the test may match across suites we use full object michael@0: // path as a name to avoid overriding same function. michael@0: target[prefix + key] = function(options) { michael@0: michael@0: // Creating `assert` functions for this test. michael@0: let assert = Assert(options); michael@0: assert.end = () => options.done(); michael@0: michael@0: // If test function is a generator use a task JS to allow yield-ing michael@0: // style test runs. michael@0: if (test.isGenerator && test.isGenerator()) { michael@0: options.waitUntilDone(); michael@0: Task.spawn(test.bind(null, assert)). michael@0: then(null, assert.fail). michael@0: then(assert.end); michael@0: } michael@0: michael@0: // If CommonJS test function expects more than one argument michael@0: // it means that test is async and second argument is a callback michael@0: // to notify that test is finished. michael@0: else if (1 < test.length) { michael@0: michael@0: // Letting test runner know that test is executed async and michael@0: // creating a callback function that CommonJS tests will call michael@0: // once it's done. michael@0: options.waitUntilDone(); michael@0: test(assert, function() { michael@0: options.done(); michael@0: }); michael@0: } michael@0: michael@0: // Otherwise CommonJS test is synchronous so we call it only with michael@0: // one argument. michael@0: else { michael@0: test(assert); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If it's an object then it's a test suite containing test function michael@0: // and / or nested test suites. In that case we just extend prefix used michael@0: // and call this function to copy and wrap tests from nested suite. michael@0: else if (isObject(test)) { michael@0: // We need to clone `tests` instead of modifying it, since it's very michael@0: // likely that it is frozen (usually test suites imported modules). michael@0: test = extend(Object.prototype, test, { michael@0: Assert: test.Assert || Assert michael@0: }); michael@0: defineTestSuite(target, test, prefix + key + "."); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * This function is a CommonJS test runner function, but since Jetpack test michael@0: * runner and test format is different from CommonJS this function shims given michael@0: * `exports` with all its tests into a Jetpack test format so that the built-in michael@0: * test runner will be able to run CommonJS test without manual changes. michael@0: */ michael@0: exports.run = function run(exports) { michael@0: michael@0: // We can't leave old properties on exports since those are test in a CommonJS michael@0: // format that why we move everything to a new `suite` object. michael@0: let suite = {}; michael@0: Object.keys(exports).forEach(function(key) { michael@0: suite[key] = exports[key]; michael@0: delete exports[key]; michael@0: }); michael@0: michael@0: // Now we wrap all the CommonJS tests to a Jetpack format and define michael@0: // those to a given `exports` object since that where jetpack test runner michael@0: // will look for them. michael@0: defineTestSuite(exports, suite); michael@0: };