diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index b7029468..39c2c7c3 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -309,12 +309,14 @@ jasmineRequire.HtmlSpecFilter = function(j$) { * @deprecated Use {@link HtmlReporterV2Urls} instead. */ function HtmlSpecFilter(options) { - j$.getEnv().deprecated( + const env = options?.env ?? j$.getEnv(); + env.deprecated( 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.' ); const filterString = options && + options.filterString && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); const filterPattern = new RegExp(filterString); diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 7202fed7..9193a12c 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -44,9 +44,14 @@ var getJasmineRequireObj = (function() { } getJasmineRequire().core = function(jRequire) { - const j$ = { private: {} }; + const j$ = {}; + Object.defineProperty(j$, 'private', { + enumerable: true, + value: {} + }); jRequire.base(j$, globalThis); + j$.private.deprecateMonkeyPatching = jRequire.deprecateMonkeyPatching(j$); j$.private.util = jRequire.util(j$); j$.private.errors = jRequire.errors(); j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$); @@ -56,7 +61,7 @@ var getJasmineRequireObj = (function() { j$.private.CallTracker = jRequire.CallTracker(j$); j$.private.MockDate = jRequire.MockDate(j$); j$.private.getStackClearer = jRequire.StackClearer(j$); - j$.private.Clock = jRequire.Clock(); + j$.private.Clock = jRequire.Clock(j$); j$.private.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); j$.private.Deprecator = jRequire.Deprecator(j$); j$.private.Configuration = jRequire.Configuration(j$); @@ -122,6 +127,20 @@ var getJasmineRequireObj = (function() { j$.private.loadedAsBrowserEsm = globalThis.document && !globalThis.document.currentScript; + j$.private.deprecateMonkeyPatching(j$, [ + // These are meant to be set by users. + 'DEFAULT_TIMEOUT_INTERVAL', + 'MAX_PRETTY_PRINT_ARRAY_LENGTH', + 'MAX_PRETTY_PRINT_CHARS', + 'MAX_PRETTY_PRINT_DEPTH', + + // These are part of the deprecation warning mechanism. To avoid infinite + // recursion, they're separately protected in a way that doesn't emit + // deprecation warnings. + 'private', + 'getEnv' + ]); + return j$; }; @@ -246,12 +265,15 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { * @function * @return {Env} */ - j$.getEnv = function(options) { - const env = (j$.private.currentEnv_ = - j$.private.currentEnv_ || new j$.private.Env(options)); - //jasmine. singletons in here (setTimeout blah blah). - return env; - }; + Object.defineProperty(j$, 'getEnv', { + enumerable: true, + value: function(options) { + const env = (j$.private.currentEnv_ = + j$.private.currentEnv_ || new j$.private.Env(options)); + //jasmine. singletons in here (setTimeout blah blah). + return env; + } + }); j$.private.isArray = function(value) { return j$.private.isA('Array', value); @@ -1520,10 +1542,13 @@ getJasmineRequireObj().Env = function(j$) { * @param {String|Error} deprecation The deprecation message * @param {Object} [options] Optional extra options, as described above */ - this.deprecated = function(deprecation, options) { - const runable = runner.currentRunable() || topSuite; - deprecator.addDeprecationWarning(runable, deprecation, options); - }; + Object.defineProperty(this, 'deprecated', { + enumerable: true, + value: function(deprecation, options) { + const runable = runner.currentRunable() || topSuite; + deprecator.addDeprecationWarning(runable, deprecation, options); + } + }); function runQueue(options) { options.clearStack = options.clearStack || stackClearer; @@ -1550,7 +1575,8 @@ getJasmineRequireObj().Env = function(j$) { runQueue }); topSuite = suiteBuilder.topSuite; - const deprecator = new j$.private.Deprecator(topSuite); + const deprecator = + envOptions?.deprecator ?? new j$.private.Deprecator(topSuite); /** * Provides the root suite, through which all suites and specs can be @@ -2055,6 +2081,8 @@ getJasmineRequireObj().Env = function(j$) { this.cleanup_ = function() { uninstallGlobalErrors(); }; + + j$.private.deprecateMonkeyPatching(this, ['deprecated']); } function indirectCallerFilename(depth) { @@ -2927,7 +2955,7 @@ getJasmineRequireObj().CallTracker = function(j$) { return CallTracker; }; -getJasmineRequireObj().Clock = function() { +getJasmineRequireObj().Clock = function(j$) { 'use strict'; /* global process */ @@ -3120,6 +3148,9 @@ callbacks to execute _before_ running the next one. clearTimeout[IsMockClockTimingFn] = true; setInterval[IsMockClockTimingFn] = true; clearInterval[IsMockClockTimingFn] = true; + + j$.private.deprecateMonkeyPatching(this); + return this; // Advances the Clock's time until the mode changes. @@ -3828,6 +3859,29 @@ getJasmineRequireObj().DelayedFunctionScheduler = function(j$) { return DelayedFunctionScheduler; }; +getJasmineRequireObj().deprecateMonkeyPatching = function(j$) { + return function deprecateMonkeyPatching(obj, keysToSkip) { + for (const key of Object.keys(obj)) { + if (!keysToSkip?.includes(key)) { + let value = obj[key]; + + Object.defineProperty(obj, key, { + enumerable: key in obj, + get() { + return value; + }, + set(newValue) { + j$.getEnv().deprecated( + 'Monkey patching detected. This is not supported and will break in a future jasmine-core release.' + ); + value = newValue; + } + }); + } + } + }; +}; + getJasmineRequireObj().Deprecator = function(j$) { 'use strict'; @@ -9378,6 +9432,7 @@ getJasmineRequireObj().interface = function(jasmine, env) { */ jasmine: jasmine }; + const existingKeys = Object.keys(jasmine); /** * Add a custom equality tester for the current scope of specs. @@ -9544,6 +9599,8 @@ getJasmineRequireObj().interface = function(jasmine, env) { * @namespace asymmetricEqualityTesters */ + jasmine.private.deprecateMonkeyPatching(jasmine, existingKeys); + return jasmineInterface; }; diff --git a/spec/core/ClockSpec.js b/spec/core/ClockSpec.js index a4d92f85..719fbca1 100644 --- a/spec/core/ClockSpec.js +++ b/spec/core/ClockSpec.js @@ -1247,4 +1247,25 @@ describe('Clock (acceptance)', function() { clock.tick(400); }); + + describe('Warning about monkey patching', function() { + for (const name of ['tick', 'mockDate', 'install', 'uninstall']) { + it(`warns if Clock#${name} is monkey patched`, function() { + spyOn(console, 'error'); + const clock = new privateUnderTest.Clock({}, function() {}, {}); + const patch = {}; + clock[name] = patch; + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('DEPRECATION: Monkey patching detected.') + ); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('ClockSpec.js') + ); + expect(clock[name]).toBe(patch); + }); + } + }); }); diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 8aa1954a..19b2148f 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -26,8 +26,6 @@ describe('Env', function() { describe('#topSuite', function() { it('returns an object that describes the tree of suites and specs', function() { - spyOn(env, 'deprecated'); - env.it('a top level spec'); env.describe('a suite', function() { env.it('a spec'); @@ -123,7 +121,6 @@ describe('Env', function() { }); it('ignores configuration properties that are present but undefined', function() { - spyOn(env, 'deprecated'); const initialConfig = { random: true, seed: '123', @@ -763,7 +760,6 @@ describe('Env', function() { it("does not expose the suite as 'this'", function() { let suiteThis; - spyOn(env, 'deprecated'); env.describe('a suite', function() { suiteThis = this; @@ -878,4 +874,37 @@ describe('Env', function() { }).toThrowError('Jasmine cannot be configured via Env in parallel mode'); }); }); + + describe('Warning about monkey patching', function() { + const names = [ + 'describe', + 'xdescribe', + 'fdescribe', + 'it', + 'xit', + 'fit', + 'beforeEach', + 'afterEach', + 'beforeAll', + 'afterAll' + ]; + + for (const name of names) { + it(`warns if Env#${name} is monkey patched`, function() { + spyOn(console, 'error'); + const patch = {}; + env[name] = patch; + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('DEPRECATION: Monkey patching detected.') + ); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('EnvSpec.js') + ); + expect(env[name]).toBe(patch); + }); + } + }); }); diff --git a/spec/core/baseSpec.js b/spec/core/baseSpec.js index 76288ec5..788ae76e 100644 --- a/spec/core/baseSpec.js +++ b/spec/core/baseSpec.js @@ -170,8 +170,15 @@ describe('base helpers', function() { }); describe('debugLog', function() { + beforeEach(function() { + privateUnderTest.currentEnv_ = jasmine.createSpyObj('env', ['debugLog']); + }); + + afterEach(function() { + privateUnderTest.currentEnv_ = null; + }); + it("forwards to the current env's debugLog function", function() { - spyOn(jasmineUnderTest.getEnv(), 'debugLog'); jasmineUnderTest.debugLog('a message'); expect(jasmineUnderTest.getEnv().debugLog).toHaveBeenCalledWith( 'a message' diff --git a/spec/core/jasmineNamespaceSpec.js b/spec/core/jasmineNamespaceSpec.js index 1e004bc1..20f55925 100644 --- a/spec/core/jasmineNamespaceSpec.js +++ b/spec/core/jasmineNamespaceSpec.js @@ -13,7 +13,72 @@ describe('The jasmine namespace', function() { expect(setDifference(actualKeys, expectedKeys())).toEqual(new Set()); }); - function expectedKeys() { + describe('Warning about monkey patching', function() { + beforeEach(function() { + spyOn(console, 'error'); + }); + + for (const key of expectedKeys(false)) { + if (!key.startsWith('MAX_') && key !== 'private' && key !== 'getEnv') { + describe(`jasmine.${key}`, function() { + let orig; + + beforeEach(function() { + orig = jasmineUnderTest[key]; + }); + + afterEach(function() { + jasmineUnderTest[key] = orig; + }); + + it('warns if monkey patched', function() { + const patch = {}; + jasmineUnderTest[key] = patch; + + verifyDeprecation(); + expect(jasmineUnderTest[key]).toBe(patch); + }); + }); + } + } + + // These specs rely on jasmineRequire being exposed. That only happens + // in browsers. + if (typeof document !== 'undefined') { + const statics = ['addMatchers', 'clock', 'createSpyObj']; + + for (const name of statics) { + describe(`jasmine.${name}`, function() { + let bootedCore, env, orig; + + beforeEach(function() { + bootedCore = jasmineRequire.core(jasmineRequire); + env = bootedCore.getEnv(); + jasmineRequire.interface(bootedCore, env); + orig = bootedCore[name]; + }); + + afterEach(function() { + bootedCore[name] = orig; + env.cleanup_(); + }); + + it(`warns if jasmine.${name} is monkey patched`, function() { + const patch = {}; + bootedCore[name] = patch; + + verifyDeprecation(); + expect(bootedCore[name]).toBe(patch); + }); + }); + } + } + }); + + function expectedKeys(includeHtml) { + if (includeHtml === undefined) { + includeHtml = typeof window !== 'undefined'; + } // Does not include properties added by requireInterface(), since that isn't // called by defineJasmineUnderTest.js/nodeDefineJasmineUnderTest.js. const result = new Set([ @@ -51,7 +116,7 @@ describe('The jasmine namespace', function() { 'getGlobal' ]); - if (typeof window !== 'undefined') { + if (includeHtml) { // jasmine-html.js result.add('HtmlReporter'); result.add('HtmlReporterV2'); @@ -76,4 +141,15 @@ describe('The jasmine namespace', function() { return result; } + + function verifyDeprecation() { + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('DEPRECATION: Monkey patching detected.') + ); + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining('jasmineNamespaceSpec.js') + ); + } }); diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index bee97735..11627c87 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -1,9 +1,12 @@ describe('HtmlReporter', function() { - let env; + let env, deprecator; beforeEach(function() { - env = new privateUnderTest.Env(); - spyOn(env, 'deprecated'); + deprecator = jasmine.createSpyObj('deprecator', [ + 'verboseDeprecations', + 'addDeprecationWarning' + ]); + env = new privateUnderTest.Env({ deprecator }); }); afterEach(function() { @@ -21,8 +24,10 @@ describe('HtmlReporter', function() { }); reporter.initialize(); - expect(env.deprecated).toHaveBeenCalledWith( - 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.' + expect(deprecator.addDeprecationWarning).toHaveBeenCalledWith( + jasmine.anything(), + 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.', + undefined ); }); diff --git a/spec/html/HtmlSpecFilterSpec.js b/spec/html/HtmlSpecFilterSpec.js index d9d60afe..83fbaa46 100644 --- a/spec/html/HtmlSpecFilterSpec.js +++ b/spec/html/HtmlSpecFilterSpec.js @@ -1,17 +1,29 @@ describe('HtmlSpecFilter', function() { + let env, deprecator; + beforeEach(function() { - spyOn(jasmineUnderTest.getEnv(), 'deprecated'); + deprecator = jasmine.createSpyObj('deprecator', [ + 'verboseDeprecations', + 'addDeprecationWarning' + ]); + env = new privateUnderTest.Env({ deprecator }); + }); + + afterEach(function() { + env.cleanup_(); }); it('emits a deprecation warning', function() { - new jasmineUnderTest.HtmlSpecFilter(); - expect(jasmineUnderTest.getEnv().deprecated).toHaveBeenCalledWith( - 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.' + new jasmineUnderTest.HtmlSpecFilter({ env }); + expect(deprecator.addDeprecationWarning).toHaveBeenCalledWith( + jasmine.anything(), + 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.', + undefined ); }); it('should match when no string is provided', function() { - const specFilter = new jasmineUnderTest.HtmlSpecFilter(); + const specFilter = new jasmineUnderTest.HtmlSpecFilter({ env }); expect(specFilter.matches('foo')).toBe(true); expect(specFilter.matches('*bar')).toBe(true); @@ -19,6 +31,7 @@ describe('HtmlSpecFilter', function() { it('should only match the provided string', function() { const specFilter = new jasmineUnderTest.HtmlSpecFilter({ + env, filterString: function() { return 'foo'; } diff --git a/src/core/Clock.js b/src/core/Clock.js index dd018113..902e1e64 100644 --- a/src/core/Clock.js +++ b/src/core/Clock.js @@ -1,4 +1,4 @@ -getJasmineRequireObj().Clock = function() { +getJasmineRequireObj().Clock = function(j$) { 'use strict'; /* global process */ @@ -191,6 +191,9 @@ callbacks to execute _before_ running the next one. clearTimeout[IsMockClockTimingFn] = true; setInterval[IsMockClockTimingFn] = true; clearInterval[IsMockClockTimingFn] = true; + + j$.private.deprecateMonkeyPatching(this); + return this; // Advances the Clock's time until the mode changes. diff --git a/src/core/Env.js b/src/core/Env.js index c6e5d39b..7413776a 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -296,10 +296,13 @@ getJasmineRequireObj().Env = function(j$) { * @param {String|Error} deprecation The deprecation message * @param {Object} [options] Optional extra options, as described above */ - this.deprecated = function(deprecation, options) { - const runable = runner.currentRunable() || topSuite; - deprecator.addDeprecationWarning(runable, deprecation, options); - }; + Object.defineProperty(this, 'deprecated', { + enumerable: true, + value: function(deprecation, options) { + const runable = runner.currentRunable() || topSuite; + deprecator.addDeprecationWarning(runable, deprecation, options); + } + }); function runQueue(options) { options.clearStack = options.clearStack || stackClearer; @@ -326,7 +329,8 @@ getJasmineRequireObj().Env = function(j$) { runQueue }); topSuite = suiteBuilder.topSuite; - const deprecator = new j$.private.Deprecator(topSuite); + const deprecator = + envOptions?.deprecator ?? new j$.private.Deprecator(topSuite); /** * Provides the root suite, through which all suites and specs can be @@ -831,6 +835,8 @@ getJasmineRequireObj().Env = function(j$) { this.cleanup_ = function() { uninstallGlobalErrors(); }; + + j$.private.deprecateMonkeyPatching(this, ['deprecated']); } function indirectCallerFilename(depth) { diff --git a/src/core/base.js b/src/core/base.js index b7cf8606..2680987a 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -67,12 +67,15 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { * @function * @return {Env} */ - j$.getEnv = function(options) { - const env = (j$.private.currentEnv_ = - j$.private.currentEnv_ || new j$.private.Env(options)); - //jasmine. singletons in here (setTimeout blah blah). - return env; - }; + Object.defineProperty(j$, 'getEnv', { + enumerable: true, + value: function(options) { + const env = (j$.private.currentEnv_ = + j$.private.currentEnv_ || new j$.private.Env(options)); + //jasmine. singletons in here (setTimeout blah blah). + return env; + } + }); j$.private.isArray = function(value) { return j$.private.isA('Array', value); diff --git a/src/core/deprecateMonkeyPatching.js b/src/core/deprecateMonkeyPatching.js new file mode 100644 index 00000000..ca73e764 --- /dev/null +++ b/src/core/deprecateMonkeyPatching.js @@ -0,0 +1,22 @@ +getJasmineRequireObj().deprecateMonkeyPatching = function(j$) { + return function deprecateMonkeyPatching(obj, keysToSkip) { + for (const key of Object.keys(obj)) { + if (!keysToSkip?.includes(key)) { + let value = obj[key]; + + Object.defineProperty(obj, key, { + enumerable: key in obj, + get() { + return value; + }, + set(newValue) { + j$.getEnv().deprecated( + 'Monkey patching detected. This is not supported and will break in a future jasmine-core release.' + ); + value = newValue; + } + }); + } + } + }; +}; diff --git a/src/core/requireCore.js b/src/core/requireCore.js index e413d6b1..e8b70c20 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -20,9 +20,14 @@ var getJasmineRequireObj = (function() { } getJasmineRequire().core = function(jRequire) { - const j$ = { private: {} }; + const j$ = {}; + Object.defineProperty(j$, 'private', { + enumerable: true, + value: {} + }); jRequire.base(j$, globalThis); + j$.private.deprecateMonkeyPatching = jRequire.deprecateMonkeyPatching(j$); j$.private.util = jRequire.util(j$); j$.private.errors = jRequire.errors(); j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$); @@ -32,7 +37,7 @@ var getJasmineRequireObj = (function() { j$.private.CallTracker = jRequire.CallTracker(j$); j$.private.MockDate = jRequire.MockDate(j$); j$.private.getStackClearer = jRequire.StackClearer(j$); - j$.private.Clock = jRequire.Clock(); + j$.private.Clock = jRequire.Clock(j$); j$.private.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); j$.private.Deprecator = jRequire.Deprecator(j$); j$.private.Configuration = jRequire.Configuration(j$); @@ -98,6 +103,20 @@ var getJasmineRequireObj = (function() { j$.private.loadedAsBrowserEsm = globalThis.document && !globalThis.document.currentScript; + j$.private.deprecateMonkeyPatching(j$, [ + // These are meant to be set by users. + 'DEFAULT_TIMEOUT_INTERVAL', + 'MAX_PRETTY_PRINT_ARRAY_LENGTH', + 'MAX_PRETTY_PRINT_CHARS', + 'MAX_PRETTY_PRINT_DEPTH', + + // These are part of the deprecation warning mechanism. To avoid infinite + // recursion, they're separately protected in a way that doesn't emit + // deprecation warnings. + 'private', + 'getEnv' + ]); + return j$; }; diff --git a/src/core/requireInterface.js b/src/core/requireInterface.js index a8833bb1..0e6e931a 100644 --- a/src/core/requireInterface.js +++ b/src/core/requireInterface.js @@ -369,6 +369,7 @@ getJasmineRequireObj().interface = function(jasmine, env) { */ jasmine: jasmine }; + const existingKeys = Object.keys(jasmine); /** * Add a custom equality tester for the current scope of specs. @@ -535,5 +536,7 @@ getJasmineRequireObj().interface = function(jasmine, env) { * @namespace asymmetricEqualityTesters */ + jasmine.private.deprecateMonkeyPatching(jasmine, existingKeys); + return jasmineInterface; }; diff --git a/src/html/HtmlSpecFilter.js b/src/html/HtmlSpecFilter.js index e86cfee9..0dace355 100644 --- a/src/html/HtmlSpecFilter.js +++ b/src/html/HtmlSpecFilter.js @@ -7,12 +7,14 @@ jasmineRequire.HtmlSpecFilter = function(j$) { * @deprecated Use {@link HtmlReporterV2Urls} instead. */ function HtmlSpecFilter(options) { - j$.getEnv().deprecated( + const env = options?.env ?? j$.getEnv(); + env.deprecated( 'HtmlReporter and HtmlSpecFilter are deprecated. Use HtmlReporterV2 instead.' ); const filterString = options && + options.filterString && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); const filterPattern = new RegExp(filterString);