diff --git a/package.json b/package.json index 6ab340f3586..b2dc87bc10e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "aws-sdk": "^2.345.0", "babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-istanbul": "^5.1.0", + "babel-plugin-mockable-imports": "^1.1.0", "babel-plugin-transform-async-to-promises": "^0.8.6", "babel-preset-env": "^1.7.0", "babelify": "^10.0.0", diff --git a/src/karma.config.js b/src/karma.config.js index 3c7eee7aed3..68a9e030a74 100644 --- a/src/karma.config.js +++ b/src/karma.config.js @@ -86,6 +86,7 @@ module.exports = function(config) { // code coverage instrumentation for Istanbul. extensions: ['.js', '.coffee'], plugins: [ + 'mockable-imports', [ 'babel-plugin-istanbul', { diff --git a/src/sidebar/components/test/group-list-item-out-of-scope-test.js b/src/sidebar/components/test/group-list-item-out-of-scope-test.js index 5f445059af3..cfaf392dcd8 100644 --- a/src/sidebar/components/test/group-list-item-out-of-scope-test.js +++ b/src/sidebar/components/test/group-list-item-out-of-scope-test.js @@ -1,16 +1,14 @@ 'use strict'; const { mount } = require('enzyme'); -const preact = require('preact'); const { createElement } = require('preact'); -const proxyquire = require('proxyquire'); const { events } = require('../../services/analytics'); +const GroupListItemOutOfScope = require('../group-list-item-out-of-scope'); describe('GroupListItemOutOfScope', () => { let fakeAnalytics; let fakeGroupListItemCommon; - let GroupListItemOutOfScope; const fakeGroup = { id: 'groupid', @@ -28,27 +26,23 @@ describe('GroupListItemOutOfScope', () => { .first() .simulate('click'); - before(() => { + beforeEach(() => { + fakeAnalytics = { + track: sinon.stub(), + events, + }; fakeGroupListItemCommon = { orgName: sinon.stub(), trackViewGroupActivity: sinon.stub(), }; - GroupListItemOutOfScope = proxyquire('../group-list-item-out-of-scope', { - // Use same instance of Preact module in tests and mocked module. - // See https://robertknight.me.uk/posts/browserify-dependency-mocking/ - preact, - + GroupListItemOutOfScope.$imports.$mock({ '../util/group-list-item-common': fakeGroupListItemCommon, - '@noCallThru': true, }); }); - beforeEach(() => { - fakeAnalytics = { - track: sinon.stub(), - events, - }; + afterEach(() => { + GroupListItemOutOfScope.$imports.$restore(); }); const createGroupListItemOutOfScope = fakeGroup => { diff --git a/src/sidebar/components/test/group-list-item-test.js b/src/sidebar/components/test/group-list-item-test.js index 39ca769e6ad..a6545fe7abb 100644 --- a/src/sidebar/components/test/group-list-item-test.js +++ b/src/sidebar/components/test/group-list-item-test.js @@ -2,7 +2,7 @@ const { createElement } = require('preact'); const { mount } = require('enzyme'); -const proxyquire = require('proxyquire'); +const GroupListItem = require('../group-list-item'); const { events } = require('../../services/analytics'); @@ -10,19 +10,6 @@ describe('GroupListItem', () => { let fakeAnalytics; let fakeStore; let fakeGroupListItemCommon; - let GroupListItem; - - before(() => { - fakeGroupListItemCommon = { - orgName: sinon.stub(), - trackViewGroupActivity: sinon.stub(), - }; - - GroupListItem = proxyquire('../group-list-item', { - '../util/group-list-item-common': fakeGroupListItemCommon, - '@noCallThru': true, - }); - }); beforeEach(() => { fakeStore = { @@ -34,6 +21,19 @@ describe('GroupListItem', () => { track: sinon.stub(), events, }; + + fakeGroupListItemCommon = { + orgName: sinon.stub(), + trackViewGroupActivity: sinon.stub(), + }; + + GroupListItem.$imports.$mock({ + '../util/group-list-item-common': fakeGroupListItemCommon, + }); + }); + + afterEach(() => { + GroupListItem.$imports.$restore(); }); const createGroupListItem = fakeGroup => { diff --git a/src/sidebar/components/test/hypothesis-app-test.js b/src/sidebar/components/test/hypothesis-app-test.js index 7fb72fb5b2c..799c3f2eebd 100644 --- a/src/sidebar/components/test/hypothesis-app-test.js +++ b/src/sidebar/components/test/hypothesis-app-test.js @@ -1,17 +1,16 @@ 'use strict'; const angular = require('angular'); -const proxyquire = require('proxyquire'); const events = require('../../events'); const bridgeEvents = require('../../../shared/bridge-events'); -const util = require('../../../shared/test/util'); + +const hypothesisApp = require('../hypothesis-app'); describe('sidebar.components.hypothesis-app', function() { let $componentController = null; let $scope = null; let $rootScope = null; - let fakeAnnotationMetadata = null; let fakeStore = null; let fakeAnalytics = null; let fakeAuth = null; @@ -44,24 +43,17 @@ describe('sidebar.components.hypothesis-app', function() { }); beforeEach(function() { - fakeAnnotationMetadata = { - location: function() { - return 0; - }, - }; - fakeServiceConfig = sandbox.stub(); - const component = proxyquire( - '../hypothesis-app', - util.noCallThru({ - angular: angular, - '../annotation-metadata': fakeAnnotationMetadata, - '../service-config': fakeServiceConfig, - }) - ); + hypothesisApp.$imports.$mock({ + '../service-config': fakeServiceConfig, + }); + + angular.module('h', []).component('hypothesisApp', hypothesisApp); + }); - angular.module('h', []).component('hypothesisApp', component); + afterEach(() => { + hypothesisApp.$imports.$restore(); }); beforeEach(angular.mock.module('h')); diff --git a/src/sidebar/components/test/login-control-test.js b/src/sidebar/components/test/login-control-test.js index 4cb24edc8bc..9e9c11c8a95 100644 --- a/src/sidebar/components/test/login-control-test.js +++ b/src/sidebar/components/test/login-control-test.js @@ -1,11 +1,12 @@ 'use strict'; const angular = require('angular'); -const proxyquire = require('proxyquire'); const bridgeEvents = require('../../../shared/bridge-events'); const util = require('../../directive/test/util'); +const loginControl = require('../login-control'); + function pageObject(element) { return { menuLinks: function() { @@ -79,19 +80,15 @@ function thirdPartyUserPage() { describe('loginControl', function() { let fakeBridge; - const fakeServiceConfig = sinon.stub(); + let fakeServiceConfig; let fakeWindow; before(function() { - angular.module('app', []).component( - 'loginControl', - proxyquire('../login-control', { - '../service-config': fakeServiceConfig, - }) - ); + angular.module('app', []).component('loginControl', loginControl); }); beforeEach(function() { + fakeServiceConfig = sinon.stub().returns(null); fakeBridge = { call: sinon.stub() }; const fakeServiceUrl = sinon.stub().returns('someUrl'); const fakeSettings = { @@ -106,8 +103,13 @@ describe('loginControl', function() { $window: fakeWindow, }); - fakeServiceConfig.reset(); - fakeServiceConfig.returns(null); + loginControl.$imports.$mock({ + '../service-config': fakeServiceConfig, + }); + }); + + afterEach(() => { + loginControl.$imports.$restore(); }); describe('the user profile button', function() { diff --git a/src/sidebar/components/test/markdown-test.js b/src/sidebar/components/test/markdown-test.js index 09cd0fbe91c..768d913de88 100644 --- a/src/sidebar/components/test/markdown-test.js +++ b/src/sidebar/components/test/markdown-test.js @@ -1,10 +1,9 @@ 'use strict'; const angular = require('angular'); -const proxyquire = require('proxyquire'); const util = require('../../directive/test/util'); -const noCallThru = require('../../../shared/test/util').noCallThru; +const markdown = require('../markdown'); describe('markdown', function() { function isHidden(element) { @@ -40,46 +39,39 @@ describe('markdown', function() { } before(function() { - angular.module('app').component( - 'markdown', - proxyquire( - '../markdown', - noCallThru({ - angular: angular, - katex: { - renderToString: function(input) { - return 'math:' + input.replace(/$$/g, ''); - }, - }, - 'lodash.debounce': function(fn) { - // Make input change debouncing synchronous in tests - return function() { - fn(); - }; - }, - '../render-markdown': noCallThru(function(markdown) { - return 'rendered:' + markdown; - }), - - '../markdown-commands': { - convertSelectionToLink: mockFormattingCommand, - toggleBlockStyle: mockFormattingCommand, - toggleSpanStyle: mockFormattingCommand, - LinkType: require('../../markdown-commands').LinkType, - }, - '../media-embedder': noCallThru({ - replaceLinksWithEmbeds: function(element) { - // Tag the element as having been processed - element.dataset.replacedLinksWithEmbeds = 'yes'; - }, - }), - }) - ) - ); + angular.module('app', []).component('markdown', markdown); }); beforeEach(function() { angular.mock.module('app'); + + markdown.$imports.$mock({ + 'lodash.debounce': function(fn) { + // Make input change debouncing synchronous in tests + return function() { + fn(); + }; + }, + '../render-markdown': markdown => { + return 'rendered:' + markdown; + }, + '../markdown-commands': { + convertSelectionToLink: mockFormattingCommand, + toggleBlockStyle: mockFormattingCommand, + toggleSpanStyle: mockFormattingCommand, + LinkType: require('../../markdown-commands').LinkType, + }, + '../media-embedder': { + replaceLinksWithEmbeds: function(element) { + // Tag the element as having been processed + element.dataset.replacedLinksWithEmbeds = 'yes'; + }, + }, + }); + }); + + afterEach(() => { + markdown.$imports.$restore(); }); describe('read only state', function() { diff --git a/src/sidebar/components/test/top-bar-test.js b/src/sidebar/components/test/top-bar-test.js index af53471e8ac..98281e272f5 100644 --- a/src/sidebar/components/test/top-bar-test.js +++ b/src/sidebar/components/test/top-bar-test.js @@ -1,24 +1,18 @@ 'use strict'; const angular = require('angular'); -const proxyquire = require('proxyquire'); +const topBar = require('../top-bar'); const util = require('../../directive/test/util'); describe('topBar', function() { const fakeSettings = {}; - const fakeIsThirdPartyService = sinon.stub(); + let fakeIsThirdPartyService; before(function() { angular .module('app', []) - .component( - 'topBar', - proxyquire('../top-bar', { - '../util/is-third-party-service': fakeIsThirdPartyService, - '@noCallThru': true, - }) - ) + .component('topBar', topBar) .component('loginControl', { bindings: require('../login-control').bindings, }) @@ -35,8 +29,15 @@ describe('topBar', function() { settings: fakeSettings, }); - fakeIsThirdPartyService.reset(); - fakeIsThirdPartyService.returns(false); + fakeIsThirdPartyService = sinon.stub().returns(false); + + topBar.$imports.$mock({ + '../util/is-third-party-service': fakeIsThirdPartyService, + }); + }); + + afterEach(() => { + topBar.$imports.$restore(); }); function applyUpdateBtn(el) { diff --git a/src/sidebar/services/test/service-url-test.js b/src/sidebar/services/test/service-url-test.js index 15eeb62adaa..2aca5e988f6 100644 --- a/src/sidebar/services/test/service-url-test.js +++ b/src/sidebar/services/test/service-url-test.js @@ -1,6 +1,6 @@ 'use strict'; -const proxyquire = require('proxyquire'); +const serviceUrlFactory = require('../service-url'); /** Return a fake store object. */ function fakeStore() { @@ -20,7 +20,7 @@ function createServiceUrl(linksPromise) { .stub() .returns({ url: 'EXPANDED_URL', params: {} }); - const serviceUrlFactory = proxyquire('../service-url', { + serviceUrlFactory.$imports.$mock({ '../util/url-util': { replaceURLParams: replaceURLParams }, }); @@ -45,6 +45,7 @@ describe('sidebar.service-url', function() { afterEach(function() { console.warn.restore(); + serviceUrlFactory.$imports.$restore(); }); context('before the API response has been received', function() { diff --git a/src/sidebar/test/raven-test.js b/src/sidebar/test/raven-test.js index a77517b4b6b..4b7d94ab544 100644 --- a/src/sidebar/test/raven-test.js +++ b/src/sidebar/test/raven-test.js @@ -1,7 +1,6 @@ 'use strict'; -const proxyquire = require('proxyquire'); -const noCallThru = require('../../shared/test/util').noCallThru; +const raven = require('../raven'); function fakeExceptionData(scriptURL) { return { @@ -28,7 +27,6 @@ describe('raven', function() { let fakeAngularTransformer; let fakeAngularPlugin; let fakeRavenJS; - let raven; beforeEach(function() { fakeRavenJS = { @@ -52,13 +50,14 @@ describe('raven', function() { Raven.setDataCallback(fakeAngularTransformer); }); - raven = proxyquire( - '../raven', - noCallThru({ - 'raven-js': fakeRavenJS, - 'raven-js/plugins/angular': fakeAngularPlugin, - }) - ); + raven.$imports.$mock({ + 'raven-js': fakeRavenJS, + 'raven-js/plugins/angular': fakeAngularPlugin, + }); + }); + + afterEach(() => { + raven.$imports.$restore(); }); describe('.install()', function() { diff --git a/src/sidebar/test/render-markdown-test.js b/src/sidebar/test/render-markdown-test.js index c3e62937ea0..d930eaeb4c3 100644 --- a/src/sidebar/test/render-markdown-test.js +++ b/src/sidebar/test/render-markdown-test.js @@ -1,13 +1,12 @@ 'use strict'; -const proxyquire = require('proxyquire'); +const renderMarkdown = require('../render-markdown'); describe('render-markdown', function() { let render; - let renderMarkdown; beforeEach(function() { - renderMarkdown = proxyquire('../render-markdown', { + renderMarkdown.$imports.$mock({ katex: { renderToString: function(input, opts) { if (opts && opts.displayMode) { @@ -23,6 +22,10 @@ describe('render-markdown', function() { }; }); + afterEach(() => { + renderMarkdown.$imports.$restore(); + }); + describe('autolinking', function() { it('should autolink URLs', function() { assert.equal( diff --git a/src/sidebar/test/virtual-thread-list-test.js b/src/sidebar/test/virtual-thread-list-test.js index 39c162908c0..4ebcb860d25 100644 --- a/src/sidebar/test/virtual-thread-list-test.js +++ b/src/sidebar/test/virtual-thread-list-test.js @@ -1,14 +1,8 @@ 'use strict'; -const proxyquire = require('proxyquire'); - -const VirtualThreadList = proxyquire('../virtual-thread-list', { - 'lodash.debounce': function(fn) { - // Make debounced functions execute immediately - return fn; - }, -}); const util = require('../../shared/test/util'); +const VirtualThreadList = require('../virtual-thread-list'); + const unroll = util.unroll; describe('VirtualThreadList', function() { @@ -45,6 +39,16 @@ describe('VirtualThreadList', function() { }; } + beforeEach(() => { + VirtualThreadList.$imports.$mock({ + 'lodash.debounce': fn => fn, + }); + }); + + afterEach(() => { + VirtualThreadList.$imports.$restore(); + }); + beforeEach(function() { fakeScope = { $digest: sinon.stub() }; diff --git a/src/sidebar/util/test/fetch-config-test.js b/src/sidebar/util/test/fetch-config-test.js index e0b95a0b817..492e72a0efa 100644 --- a/src/sidebar/util/test/fetch-config-test.js +++ b/src/sidebar/util/test/fetch-config-test.js @@ -1,13 +1,12 @@ 'use strict'; -const proxyquire = require('proxyquire'); - const { assertPromiseIsRejected, } = require('../../../shared/test/promise-util'); +const { fetchConfig, $imports } = require('../fetch-config'); + describe('sidebar.util.fetch-config', () => { - let fetchConfig; let fakeHostConfig; let fakeJsonRpc; let fakeWindow; @@ -17,11 +16,10 @@ describe('sidebar.util.fetch-config', () => { fakeJsonRpc = { call: sinon.stub(), }; - const patched = proxyquire('../fetch-config', { + $imports.$mock({ '../host-config': fakeHostConfig, './postmessage-json-rpc': fakeJsonRpc, }); - fetchConfig = patched.fetchConfig; // By default, embedder provides no custom config. fakeHostConfig.returns({}); @@ -39,6 +37,10 @@ describe('sidebar.util.fetch-config', () => { fakeWindow = { parent: fakeParent, top: fakeTopWindow }; }); + afterEach(() => { + $imports.$restore(); + }); + describe('fetchConfig', () => { // By default, combine the settings rendered into the sidebar's HTML page // by h with the settings from `window.hypothesisConfig` in the parent diff --git a/src/sidebar/util/test/is-third-party-service-test.js b/src/sidebar/util/test/is-third-party-service-test.js index f79c1334e35..7547b184c86 100644 --- a/src/sidebar/util/test/is-third-party-service-test.js +++ b/src/sidebar/util/test/is-third-party-service-test.js @@ -1,22 +1,25 @@ 'use strict'; -const proxyquire = require('proxyquire'); +const isThirdPartyService = require('../is-third-party-service'); describe('sidebar.util.isThirdPartyService', () => { let fakeServiceConfig; let fakeSettings; - let isThirdPartyService; beforeEach(() => { fakeServiceConfig = sinon.stub(); fakeSettings = { authDomain: 'hypothes.is' }; - isThirdPartyService = proxyquire('../is-third-party-service', { + isThirdPartyService.$imports.$mock({ '../service-config': fakeServiceConfig, '@noCallThru': true, }); }); + afterEach(() => { + isThirdPartyService.$imports.$restore(); + }); + it('returns false for first-party services', () => { fakeServiceConfig.returns({ authority: 'hypothes.is' }); diff --git a/yarn.lock b/yarn.lock index 3997148f8b8..b63a82ec077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1554,6 +1554,11 @@ babel-plugin-istanbul@^5.1.0: istanbul-lib-instrument "^3.0.0" test-exclude "^5.0.0" +babel-plugin-mockable-imports@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-mockable-imports/-/babel-plugin-mockable-imports-1.2.0.tgz#151db640cf6339a6a878a3b70e336eec4baa1895" + integrity sha512-KqKIFsPDQcXf2XZZazAxGa7xUgbGueLfCQb4mD7G16lvLYfz13SakC6IAcRaBueXwNn2O0K6VGlXZzPs0aLL3Q== + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"