Bug 1510545 - Move mock support to browser-loader and shared test helper;r=ochameau,ladybenko

Differential Revision: https://phabricator.services.mozilla.com/D13751

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Julian Descottes 2018-12-13 18:02:09 +00:00
parent fae5834855
commit 9e282c21e8
12 changed files with 219 additions and 99 deletions

View File

@ -11,6 +11,5 @@ DevToolsModules(
'network-locations.js',
'runtime-client-factory.js',
'runtimes-state-helper.js',
'test-helper.js',
'usb-runtimes.js',
)

View File

@ -55,5 +55,3 @@ async function createClientForRuntime(runtime) {
}
exports.createClientForRuntime = createClientForRuntime;
require("./test-helper").enableMocks(module, "modules/runtime-client-factory");

View File

@ -1,84 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const flags = require("devtools/shared/flags");
/**
* The methods from this module are going to be called from both aboutdebugging
* BrowserLoader modules (enableMocks) and from tests (setMockedModules,
* removeMockedModule). We use the main devtools loader to communicate between the
* different sandboxes involved.
*/
const { Cu } = require("chrome");
const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const ROOT_PATH = "resource://devtools/client/aboutdebugging-new/src/";
/**
* Allows to load a mock instead of an about:debugging module.
* To use it, the module to mock needs to call
* require("devtools/client/aboutdebugging-new/src/modules/test-helper").
* enableMocks(module, "path/to/module.js");
*
* From the tests, the devtools loader should be provided with the mocked module using
* setMockedModule().
*
* At the end of the test, the mocks should be discarded with removeMockedModule().
*
* @param {Object} moduleToMock
* The `module` global from the module we want to mock, in order to override its
* `module.exports`.
* @param {String} modulePath
* Path to the module, relative to `devtools/client/aboutdebugging-new/src/`
*/
function enableMocks(moduleToMock, modulePath) {
if (flags.testing) {
const path = ROOT_PATH + modulePath + ".js";
if (loader.mocks && loader.mocks[path]) {
moduleToMock.exports = loader.mocks[path];
}
}
}
exports.enableMocks = enableMocks;
/**
* Assign a mock object to the provided module path, relative to
* `devtools/client/aboutdebugging-new/src/`.
*/
function setMockedModule(mock, modulePath) {
loader.mocks = loader.mocks || {};
const mockedModule = {};
Object.keys(mock).forEach(key => {
if (typeof mock[key] === "function") {
// All the mock methods are wrapped again here so that if the test replaces the
// methods dynamically and call sites used
// `const { someMethod } = require("./runtime-client-factory")`, someMethod will
// still internally call the current method defined on the mock, and not the one
// that was defined at the moment of the import.
mockedModule[key] = function() {
return mock[key].apply(mock, arguments);
};
} else {
mockedModule[key] = mock[key];
}
});
const path = ROOT_PATH + modulePath + ".js";
loader.mocks[path] = mockedModule;
}
exports.setMockedModule = setMockedModule;
/**
* Remove any mock object defined for the provided module path, relative to
* `devtools/client/aboutdebugging-new/src/`.
*/
function removeMockedModule(modulePath) {
const path = ROOT_PATH + modulePath + ".js";
if (loader.mocks && loader.mocks[path]) {
delete loader.mocks[path];
}
}
exports.removeMockedModule = removeMockedModule;

View File

@ -29,5 +29,3 @@ function refreshUSBRuntimes() {
return adb.updateRuntimes();
}
exports.refreshUSBRuntimes = refreshUSBRuntimes;
require("./test-helper").enableMocks(module, "modules/usb-runtimes");

View File

@ -1,7 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from ../../../../shared/test/shared-head.js */
"use strict";
/**
@ -13,16 +12,18 @@
* - createClientForRuntime(runtime)
*/
function enableRuntimeClientFactoryMock(mock) {
const { setMockedModule } = require("devtools/client/aboutdebugging-new/src/modules/test-helper");
setMockedModule(mock, "modules/runtime-client-factory");
const { setMockedModule } = require("devtools/client/shared/browser-loader-mocks");
setMockedModule(mock,
"devtools/client/aboutdebugging-new/src/modules/runtime-client-factory");
}
/**
* Update the loader to clear the mock entry for the runtime-client-factory module.
*/
function disableRuntimeClientFactoryMock() {
const { removeMockedModule } = require("devtools/client/aboutdebugging-new/src/modules/test-helper");
removeMockedModule("modules/runtime-client-factory");
const { removeMockedModule } = require("devtools/client/shared/browser-loader-mocks");
removeMockedModule(
"devtools/client/aboutdebugging-new/src/modules/runtime-client-factory");
}
/**

View File

@ -17,16 +17,18 @@
* - removeUSBRuntimesObserver(listener)
*/
function enableUsbRuntimesMock(mock) {
const { setMockedModule } = require("devtools/client/aboutdebugging-new/src/modules/test-helper");
setMockedModule(mock, "modules/usb-runtimes");
const { setMockedModule } = require("devtools/client/shared/browser-loader-mocks");
setMockedModule(mock,
"devtools/client/aboutdebugging-new/src/modules/usb-runtimes");
}
/**
* Update the loader to clear the mock entry for the usb-runtimes module.
*/
function disableUsbRuntimesMock() {
const { removeMockedModule } = require("devtools/client/aboutdebugging-new/src/modules/test-helper");
removeMockedModule("modules/usb-runtimes");
const { removeMockedModule } = require("devtools/client/shared/browser-loader-mocks");
removeMockedModule(
"devtools/client/aboutdebugging-new/src/modules/usb-runtimes");
}
/**

View File

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Map of mocked modules, keys are absolute URIs for devtools modules such as
// "resource://devtools/path/to/mod.js, values are objects (anything passed to
// setMockedModule technically).
const _mocks = {};
/**
* Retrieve a mocked module matching the provided uri, eg "resource://path/to/file.js".
*/
function getMockedModule(uri) {
return _mocks[uri];
}
exports.getMockedModule = getMockedModule;
/**
* Module paths are transparently provided with or without ".js" when using the loader,
* normalize the user-provided module paths to always have modules ending with ".js".
*/
function _getUriForModulePath(modulePath) {
// Assume js modules and add the .js extension if missing.
if (!modulePath.endsWith(".js")) {
modulePath = modulePath + ".js";
}
// Add resource:// scheme if no scheme is specified.
if (!modulePath.includes("://")) {
modulePath = "resource://" + modulePath;
}
return modulePath;
}
/**
* Assign a mock object to the provided module path.
* @param mock
* Plain JavaScript object that will implement the expected API for the mocked
* module.
* @param modulePath
* The module path should be the absolute module path, starting with `devtools`:
* "devtools/client/some-panel/some-module"
*/
function setMockedModule(mock, modulePath) {
const uri = _getUriForModulePath(modulePath);
_mocks[uri] = new Proxy(mock, {
get(target, key) {
if (typeof target[key] === "function") {
// Functions are wrapped to be able to update the methods during the test, even if
// the methods were imported with destructuring. For instance:
// `const { someMethod } = require("./my-module");`
return function() {
return target[key].apply(target, arguments);
};
}
return target[key];
},
});
}
exports.setMockedModule = setMockedModule;
/**
* Remove any mock object defined for the provided absolute module path.
*/
function removeMockedModule(modulePath) {
const uri = _getUriForModulePath(modulePath);
delete _mocks[uri];
}
exports.removeMockedModule = removeMockedModule;

View File

@ -4,11 +4,18 @@
"use strict";
const loaders = ChromeUtils.import("resource://devtools/shared/base-loader.js", {});
const { devtools } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const {
devtools,
loader,
} = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const flags = devtools.require("devtools/shared/flags");
const { joinURI } = devtools.require("devtools/shared/path");
const { assert } = devtools.require("devtools/shared/DevToolsUtils");
const { AppConstants } = devtools.require("resource://gre/modules/AppConstants.jsm");
loader.lazyRequireGetter(this, "getMockedModule",
"devtools/client/shared/browser-loader-mocks", {});
const BROWSER_BASED_DIRS = [
"resource://devtools/client/inspector/boxmodel",
"resource://devtools/client/inspector/changes",
@ -128,6 +135,13 @@ function BrowserLoaderBuilder({ baseURI, window, useOnlyShared, commonLibRequire
const uri = require.resolve(id);
// The mocks can be set from tests using browser-loader-mocks.js setMockedModule().
// If there is an entry for a given uri in the `mocks` object, return it instead of
// requiring the module.
if (flags.testing && getMockedModule(uri)) {
return getMockedModule(uri);
}
if (commonLibRequire && COMMON_LIBRARY_DIRS.some(dir => uri.startsWith(dir))) {
return commonLibRequire(uri);
}

View File

@ -22,6 +22,7 @@ DIRS += [
DevToolsModules(
'autocomplete-popup.js',
'browser-loader-mocks.js',
'browser-loader.js',
'css-angle.js',
'curl.js',

View File

@ -59,12 +59,14 @@ support-files =
telemetry-test-helpers.js
test-actor-registry.js
test-actor.js
test-mocked-module.js
testactors.js
!/devtools/client/responsive.html/test/browser/devices.json
!/devtools/client/debugger/new/test/mochitest/helpers.js
!/devtools/client/debugger/new/test/mochitest/helpers/context.js
[browser_autocomplete_popup.js]
[browser_browserloader_mocks.js]
[browser_css_angle.js]
[browser_css_color.js]
[browser_cubic-bezier-01.js]

View File

@ -0,0 +1,106 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { BrowserLoader } =
ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
const { getMockedModule, setMockedModule, removeMockedModule } =
require("devtools/client/shared/browser-loader-mocks");
const { require: browserRequire } = BrowserLoader({
baseURI: "resource://devtools/client/shared/",
window,
});
// Check that modules can be mocked in the browser loader.
// Test with a custom test module under the chrome:// scheme.
function testWithChromeScheme() {
// Full chrome URI for the test module.
const CHROME_URI = CHROME_URL_ROOT + "test-mocked-module.js";
const ORIGINAL_VALUE = "Original value";
const MOCKED_VALUE_1 = "Mocked value 1";
const MOCKED_VALUE_2 = "Mocked value 2";
const m1 = browserRequire(CHROME_URI);
ok(m1, "Regular module can be required");
is(m1.methodToMock(), ORIGINAL_VALUE, "Method returns the expected value");
is(m1.someProperty, "someProperty", "Property has the expected value");
info("Create a simple mocked version of the test module");
const mockedModule = {
methodToMock: () => MOCKED_VALUE_1,
someProperty: "somePropertyMocked",
};
setMockedModule(mockedModule, CHROME_URI);
ok(!!getMockedModule(CHROME_URI), "Has an entry for the chrome URI.");
const m2 = browserRequire(CHROME_URI);
ok(m2, "Mocked module can be required via chrome URI");
is(m2.methodToMock(), MOCKED_VALUE_1, "Mocked method returns the expected value");
is(m2.someProperty, "somePropertyMocked", "Mocked property has the expected value");
const { methodToMock: requiredMethod } = browserRequire(CHROME_URI);
ok(requiredMethod() === MOCKED_VALUE_1,
"Mocked method returns the expected value when imported with destructuring");
info("Update the mocked method to return a different value");
mockedModule.methodToMock = () => MOCKED_VALUE_2;
is(requiredMethod(), MOCKED_VALUE_2,
"Mocked method returns the updated value when imported with destructuring");
info("Remove the mock for the test module");
removeMockedModule(CHROME_URI);
ok(!getMockedModule(CHROME_URI), "Has no entry for the chrome URI.");
const m3 = browserRequire(CHROME_URI);
ok(m3, "Regular module can be required after removing the mock");
is(m3.methodToMock(), ORIGINAL_VALUE, "Method on module returns the expected value");
}
// Similar tests as in testWithChromeScheme, but this time with a devtools module
// available under the resource:// scheme.
function testWithRegularDevtoolsModule() {
// Testing with devtools/shared/path because it is a simple module, that can be imported
// with destructuring. Any other module would do.
const DEVTOOLS_MODULE_PATH = "devtools/shared/path";
const DEVTOOLS_MODULE_URI = "resource://devtools/shared/path.js";
const m1 = browserRequire(DEVTOOLS_MODULE_PATH);
is(m1.joinURI("http://a", "b"), "http://a/b", "Original module was required");
info("Set a mock for a sub-part of the path, which should not match require calls");
setMockedModule({ joinURI: () => "WRONG_PATH" }, "shared/path");
ok(!getMockedModule(DEVTOOLS_MODULE_URI), "Has no mock entry for the full URI");
const m2 = browserRequire(DEVTOOLS_MODULE_PATH);
is(m2.joinURI("http://a", "b"), "http://a/b", "Original module is still required");
info("Set a mock for the complete path, which should now match require calls");
const mockedModule = {
joinURI: () => "MOCKED VALUE",
};
setMockedModule(mockedModule, DEVTOOLS_MODULE_PATH);
ok(!!getMockedModule(DEVTOOLS_MODULE_URI), "Has a mock entry for the full URI.");
const m3 = browserRequire(DEVTOOLS_MODULE_PATH);
is(m3.joinURI("http://a", "b"), "MOCKED VALUE", "The mocked module has been returned");
info("Check that the mocked methods can be updated after a destructuring import");
const { joinURI } = browserRequire(DEVTOOLS_MODULE_PATH);
mockedModule.joinURI = () => "UPDATED VALUE";
is(joinURI("http://a", "b"), "UPDATED VALUE", "Mocked method was correctly updated");
removeMockedModule(DEVTOOLS_MODULE_PATH);
ok(!getMockedModule(DEVTOOLS_MODULE_URI), "Has no mock entry for the full URI");
const m4 = browserRequire(DEVTOOLS_MODULE_PATH);
is(m4.joinURI("http://a", "b"), "http://a/b", "Original module can be required again");
}
function test() {
testWithChromeScheme();
testWithRegularDevtoolsModule();
finish();
}

View File

@ -0,0 +1,11 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const methodToMock = function() {
return "Original value";
};
exports.methodToMock = methodToMock;
exports.someProperty = "someProperty";