Detect monkey patching and emit a deprecation warning.

This isn't comprehensive but it should be broad enough to ensure that most
people who would be affected by blocking monkey patching see a warning.
Covers the jasmine namespace as well as classes that are monkey patched by
zone.js.

Replacing globals (describe/it/etc) doesn't trigger a warning because they
belong to the user and are expected to be replaced.
This commit is contained in:
Steve Gravrock
2025-11-25 16:54:08 -08:00
parent 32168be6c7
commit 23894c1a0a
15 changed files with 315 additions and 47 deletions

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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);
});
}
});
});

View File

@@ -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);
});
}
});
});

View File

@@ -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'

View File

@@ -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')
);
}
});

View File

@@ -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
);
});

View File

@@ -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';
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;
}
});
}
}
};
};

View File

@@ -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$;
};

View File

@@ -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;
};

View File

@@ -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);