diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js
index 8c098d84..9e504590 100644
--- a/lib/jasmine-core/jasmine-html.js
+++ b/lib/jasmine-core/jasmine-html.js
@@ -50,10 +50,16 @@ const getJasmineHtmlRequireObj = (function() {
private$.FailuresView = htmlRequire.FailuresView(j$, private$);
private$.PerformanceView = htmlRequire.PerformanceView(j$, private$);
private$.TabBar = htmlRequire.TabBar(j$, private$);
- j$.HtmlReporterV2Urls = htmlRequire.HtmlReporterV2Urls(j$, private$);
- j$.HtmlReporterV2 = htmlRequire.HtmlReporterV2(j$, private$);
- j$.QueryString = htmlRequire.QueryString();
private$.HtmlSpecFilterV2 = htmlRequire.HtmlSpecFilterV2();
+
+ for (const k of ['HtmlReporterV2Urls', 'HtmlReporterV2', 'QueryString']) {
+ Object.defineProperty(j$, k, {
+ enumerable: true,
+ configurable: false,
+ writable: false,
+ value: htmlRequire[k](j$, private$)
+ });
+ }
};
return getJasmineHtmlRequire;
@@ -101,6 +107,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
*/
constructor(options) {
this.#getWindowLocation = options.getWindowLocation;
+ Object.freeze(this);
}
/**
@@ -168,6 +175,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
return '?' + qStrPairs.join('&');
}
+ Object.freeze(QueryString.prototype);
return QueryString;
};
@@ -772,6 +780,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
);
this.#container.appendChild(this.#htmlReporterMain);
this.#failures.show();
+
+ Object.freeze(this);
}
jasmineStarted(options) {
@@ -949,6 +959,7 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
}
}
+ Object.freeze(HtmlReporterV2.prototype);
return HtmlReporterV2;
};
@@ -972,6 +983,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
return window.location;
}
});
+
+ Object.freeze(this);
}
/**
@@ -1022,6 +1035,7 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
}
}
+ Object.freeze(HtmlReporterV2Urls.prototype);
return HtmlReporterV2Urls;
};
diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js
index 95b3d2d7..b9612e63 100644
--- a/lib/jasmine-core/jasmine.js
+++ b/lib/jasmine-core/jasmine.js
@@ -141,6 +141,28 @@ const getJasmineRequireObj = (function() {
private$.loadedAsBrowserEsm = loadedAsBrowserEsm;
+ // Prevent monkey patching of existing properties but allow adding new ones.
+ // jasmine-html.js needs to be able to add to the jasmine namespace.
+ // jasmine-ajax also installs itself this way.
+ const writeable = [
+ 'DEFAULT_TIMEOUT_INTERVAL',
+ 'MAX_PRETTY_PRINT_ARRAY_LENGTH',
+ 'MAX_PRETTY_PRINT_CHARS',
+ 'MAX_PRETTY_PRINT_DEPTH'
+ ];
+ const descriptors = Object.getOwnPropertyDescriptors(j$);
+
+ for (const [k, d] of Object.entries(descriptors)) {
+ if (!writeable.includes(k)) {
+ Object.defineProperty(j$, k, {
+ value: d.value,
+ enumerable: d.enumerable,
+ configurable: false,
+ writable: false
+ });
+ }
+ }
+
return { jasmine: j$, private: private$ };
};
@@ -241,6 +263,7 @@ getJasmineRequireObj().base = function(j$, private$, jasmineGlobal) {
*/
let DEFAULT_TIMEOUT_INTERVAL = 5000;
Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
+ enumerable: true,
get: function() {
return DEFAULT_TIMEOUT_INTERVAL;
},
@@ -2077,6 +2100,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
this.cleanup_ = function() {
uninstallGlobalErrors();
};
+
+ Object.freeze(this);
}
function indirectCallerFilename(depth) {
@@ -2087,6 +2112,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
return frames[depth] && frames[depth].file;
}
+ Object.freeze(Env);
+ Object.freeze(Env.prototype);
return Env;
};
@@ -3009,6 +3036,8 @@ callbacks to execute _before_ running the next one.
setInterval[IsMockClockTimingFn] = true;
clearInterval[IsMockClockTimingFn] = true;
+ Object.freeze(this);
+
return this;
// Advances the Clock's time until the mode changes.
@@ -3162,6 +3191,8 @@ callbacks to execute _before_ running the next one.
};
Clock.IsMockClockTimingFn = IsMockClockTimingFn;
+ Object.freeze(Clock);
+ Object.freeze(Clock.prototype);
return Clock;
};
@@ -8038,9 +8069,12 @@ getJasmineRequireObj().ParallelReportDispatcher = function(j$, private$) {
for (const eventName of private$.reporterEvents) {
this[eventName] = dispatcher[eventName].bind(dispatcher);
}
+
+ Object.freeze(this);
}
}
+ Object.freeze(ParallelReportDispatcher.prototype);
return ParallelReportDispatcher;
};
@@ -11698,8 +11732,12 @@ getJasmineRequireObj().Timer = function() {
this.elapsed = function() {
return now() - startTime;
};
+
+ Object.freeze(this);
}
+ Object.freeze(Timer);
+ Object.freeze(Timer.prototype);
return Timer;
};
diff --git a/spec/core/ClockSpec.js b/spec/core/ClockSpec.js
index a4d92f85..7a5ad19f 100644
--- a/spec/core/ClockSpec.js
+++ b/spec/core/ClockSpec.js
@@ -1247,4 +1247,8 @@ describe('Clock (acceptance)', function() {
clock.tick(400);
});
+
+ isNonMonkeyPatchableClass(privateUnderTest.Clock, function() {
+ return new privateUnderTest.Clock({}, function() {}, {});
+ });
});
diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js
index 3da94a51..ec6c9a66 100644
--- a/spec/core/EnvSpec.js
+++ b/spec/core/EnvSpec.js
@@ -874,4 +874,8 @@ describe('Env', function() {
}).toThrowError('Jasmine cannot be configured via Env in parallel mode');
});
});
+
+ isNonMonkeyPatchableClass(privateUnderTest.Env, function() {
+ return new privateUnderTest.Env();
+ });
});
diff --git a/spec/core/ParallelReportDispatcherSpec.js b/spec/core/ParallelReportDispatcherSpec.js
index b4d2e137..e7eaf352 100644
--- a/spec/core/ParallelReportDispatcherSpec.js
+++ b/spec/core/ParallelReportDispatcherSpec.js
@@ -160,6 +160,13 @@ describe('ParallelReportDispatcher', function() {
);
});
+ isNonMonkeyPatchableClass(
+ jasmineUnderTest.ParallelReportDispatcher,
+ function() {
+ return new jasmineUnderTest.ParallelReportDispatcher();
+ }
+ );
+
function mockGlobalErrors() {
const globalErrors = jasmine.createSpyObj('globalErrors', [
'install',
diff --git a/spec/core/TimerSpec.js b/spec/core/TimerSpec.js
index 7d23d10a..1944eec5 100644
--- a/spec/core/TimerSpec.js
+++ b/spec/core/TimerSpec.js
@@ -30,4 +30,8 @@ describe('Timer', function() {
expect(timer.elapsed()).toEqual(jasmine.any(Number));
});
});
+
+ isNonMonkeyPatchableClass(jasmineUnderTest.Timer, function() {
+ return new jasmineUnderTest.Timer();
+ });
});
diff --git a/spec/core/jasmineNamespaceSpec.js b/spec/core/jasmineNamespaceSpec.js
index 93ae2c3b..bec7311b 100644
--- a/spec/core/jasmineNamespaceSpec.js
+++ b/spec/core/jasmineNamespaceSpec.js
@@ -13,13 +13,61 @@ describe('The jasmine namespace', function() {
expect(setDifference(actualKeys, expectedKeys())).toEqual(new Set());
});
+ describe('Preventing monkey patching', function() {
+ const mutable = mutableKeys();
+
+ for (const key of expectedKeys()) {
+ if (mutable.includes(key)) {
+ it(`allows overwriting of jasmine.${key}`, function() {
+ const existingVal = jasmineUnderTest[key];
+
+ try {
+ jasmineUnderTest[key] = 'new value';
+ expect(jasmineUnderTest[key]).toEqual('new value');
+ } finally {
+ jasmineUnderTest[key] = existingVal;
+ }
+ });
+ } else {
+ it(`prevents overwriting of jasmine.${key}`, function() {
+ const existingVal = jasmineUnderTest[key];
+
+ try {
+ jasmineUnderTest[key] = 'monkey patch';
+ expect(jasmineUnderTest[key]).toBe(existingVal);
+ } finally {
+ // This will be a no-op if the test passed, but will prevent state
+ // leakage if it failed.
+ jasmineUnderTest[key] = existingVal;
+ }
+ });
+ }
+ }
+
+ it('allows additions', function() {
+ try {
+ jasmineUnderTest.Ajax = 'it worked';
+ expect(jasmineUnderTest.Ajax).toEqual('it worked');
+ } finally {
+ delete jasmineUnderTest.Ajax;
+ }
+ });
+ });
+
+ function mutableKeys() {
+ return [
+ 'MAX_PRETTY_PRINT_ARRAY_LENGTH',
+ 'MAX_PRETTY_PRINT_CHARS',
+ 'MAX_PRETTY_PRINT_DEPTH',
+ 'DEFAULT_TIMEOUT_INTERVAL'
+ ];
+ }
+
function expectedKeys() {
// Does not include properties added by requireInterface(), since that isn't
// called by defineJasmineUnderTest.js/nodeDefineJasmineUnderTest.js.
const result = new Set([
- 'MAX_PRETTY_PRINT_ARRAY_LENGTH',
- 'MAX_PRETTY_PRINT_CHARS',
- 'MAX_PRETTY_PRINT_DEPTH',
+ ...mutableKeys(),
'debugLog',
'getEnv',
'isSpy',
diff --git a/spec/helpers/monkeyPatchingSpecs.js b/spec/helpers/monkeyPatchingSpecs.js
new file mode 100644
index 00000000..af06f8cf
--- /dev/null
+++ b/spec/helpers/monkeyPatchingSpecs.js
@@ -0,0 +1,67 @@
+globalThis.isNonMonkeyPatchableClass = function(ctor, makeInstance) {
+ describe('Monkey patching prevention', function() {
+ it(`prevents overwriting ${ctor.name}.prototype`, function() {
+ const existing = ctor.prototype;
+
+ try {
+ ctor.prototype = {};
+ expect(ctor.prototype).toBe(existing);
+ } finally {
+ // This will be a no-op if the test passed, but will prevent state
+ // leakage if it failed.
+ ctor.prototype = existing;
+ }
+ });
+
+ it("prevents overwriting an instance's prototype", function() {
+ const instance = makeInstance();
+ let thrown;
+
+ // The message varies from browser to browser, so we can't rely on it
+ try {
+ instance.__proto__ = {};
+ } catch (e) {
+ thrown = e;
+ }
+
+ expect(thrown).toBeInstanceOf(TypeError);
+ });
+
+ it('prevents overwriting prototype properties', function() {
+ let any = false;
+
+ for (const k of Object.getOwnPropertyNames(ctor.prototype)) {
+ any = true;
+ const existingValue = ctor.prototype[k];
+
+ try {
+ ctor.prototype[k] = {};
+ expect(ctor.prototype[k])
+ .withContext(k)
+ .toBe(existingValue);
+ } finally {
+ // This will be a no-op if the test passed, but will prevent state
+ // leakage if it failed.
+ ctor.prototype[k] = existingValue;
+ }
+ }
+
+ expect(any).toBe(true);
+ });
+
+ it('prevents overriding prototype properties', function() {
+ const instance = makeInstance();
+ let any = false;
+
+ for (const k of Object.getOwnPropertyNames(ctor.prototype)) {
+ any = true;
+ instance[k] = {};
+ expect(instance[k])
+ .withContext(k)
+ .toBe(ctor.prototype[k]);
+ }
+
+ expect(any).toBe(true);
+ });
+ });
+};
diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js
index dcc583a0..d4167f87 100644
--- a/spec/html/HtmlReporterV2Spec.js
+++ b/spec/html/HtmlReporterV2Spec.js
@@ -1396,4 +1396,6 @@ describe('HtmlReporterV2', function() {
});
});
});
+
+ isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2, setup);
});
diff --git a/spec/html/HtmlReporterV2UrlsSpec.js b/spec/html/HtmlReporterV2UrlsSpec.js
index e2b99900..64c78436 100644
--- a/spec/html/HtmlReporterV2UrlsSpec.js
+++ b/spec/html/HtmlReporterV2UrlsSpec.js
@@ -63,4 +63,8 @@ describe('HtmlReporterV2Urls', function() {
return qs;
}
});
+
+ isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2Urls, function() {
+ return new jasmineUnderTest.HtmlReporterV2Urls({});
+ });
});
diff --git a/spec/html/QueryStringSpec.js b/spec/html/QueryStringSpec.js
index bc8d1770..00ca656a 100644
--- a/spec/html/QueryStringSpec.js
+++ b/spec/html/QueryStringSpec.js
@@ -77,4 +77,12 @@ describe('QueryString', function() {
expect(queryString.getParam('baz')).toBeFalsy();
});
});
+
+ isNonMonkeyPatchableClass(jasmineUnderTest.QueryString, function() {
+ return new jasmineUnderTest.QueryString({
+ getWindowLocation: function() {
+ return { search: '' };
+ }
+ });
+ });
});
diff --git a/spec/support/jasmine-browser.js b/spec/support/jasmine-browser.js
index e19d9f1a..0771e7fc 100644
--- a/spec/support/jasmine-browser.js
+++ b/spec/support/jasmine-browser.js
@@ -25,6 +25,7 @@ module.exports = {
'helpers/domHelpers.js',
'helpers/integrationMatchers.js',
'helpers/callerFilenameShim.js',
+ 'helpers/monkeyPatchingSpecs.js',
'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js'
],
diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json
index e9094d74..3be76276 100644
--- a/spec/support/jasmine.json
+++ b/spec/support/jasmine.json
@@ -10,6 +10,7 @@
"helpers/integrationMatchers.js",
"helpers/callerFilenameShim.js",
"helpers/overrideConsoleLogForCircleCi.js",
+ "helpers/monkeyPatchingSpecs.js",
"helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js"
],
diff --git a/src/core/Clock.js b/src/core/Clock.js
index 883e84df..907fbad6 100644
--- a/src/core/Clock.js
+++ b/src/core/Clock.js
@@ -192,6 +192,8 @@ callbacks to execute _before_ running the next one.
setInterval[IsMockClockTimingFn] = true;
clearInterval[IsMockClockTimingFn] = true;
+ Object.freeze(this);
+
return this;
// Advances the Clock's time until the mode changes.
@@ -345,5 +347,7 @@ callbacks to execute _before_ running the next one.
};
Clock.IsMockClockTimingFn = IsMockClockTimingFn;
+ Object.freeze(Clock);
+ Object.freeze(Clock.prototype);
return Clock;
};
diff --git a/src/core/Env.js b/src/core/Env.js
index 6e66a76d..d44d6e7c 100644
--- a/src/core/Env.js
+++ b/src/core/Env.js
@@ -835,6 +835,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
this.cleanup_ = function() {
uninstallGlobalErrors();
};
+
+ Object.freeze(this);
}
function indirectCallerFilename(depth) {
@@ -845,5 +847,7 @@ getJasmineRequireObj().Env = function(j$, private$) {
return frames[depth] && frames[depth].file;
}
+ Object.freeze(Env);
+ Object.freeze(Env.prototype);
return Env;
};
diff --git a/src/core/ParallelReportDispatcher.js b/src/core/ParallelReportDispatcher.js
index 360478d6..72d29c8c 100644
--- a/src/core/ParallelReportDispatcher.js
+++ b/src/core/ParallelReportDispatcher.js
@@ -87,8 +87,11 @@ getJasmineRequireObj().ParallelReportDispatcher = function(j$, private$) {
for (const eventName of private$.reporterEvents) {
this[eventName] = dispatcher[eventName].bind(dispatcher);
}
+
+ Object.freeze(this);
}
}
+ Object.freeze(ParallelReportDispatcher.prototype);
return ParallelReportDispatcher;
};
diff --git a/src/core/Timer.js b/src/core/Timer.js
index a65064ba..b1726393 100644
--- a/src/core/Timer.js
+++ b/src/core/Timer.js
@@ -39,7 +39,11 @@ getJasmineRequireObj().Timer = function() {
this.elapsed = function() {
return now() - startTime;
};
+
+ Object.freeze(this);
}
+ Object.freeze(Timer);
+ Object.freeze(Timer.prototype);
return Timer;
};
diff --git a/src/core/base.js b/src/core/base.js
index 9f1fa540..523bf1c2 100644
--- a/src/core/base.js
+++ b/src/core/base.js
@@ -43,6 +43,7 @@ getJasmineRequireObj().base = function(j$, private$, jasmineGlobal) {
*/
let DEFAULT_TIMEOUT_INTERVAL = 5000;
Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
+ enumerable: true,
get: function() {
return DEFAULT_TIMEOUT_INTERVAL;
},
diff --git a/src/core/requireCore.js b/src/core/requireCore.js
index 1d153dc8..dd9b5cc4 100644
--- a/src/core/requireCore.js
+++ b/src/core/requireCore.js
@@ -116,6 +116,28 @@ const getJasmineRequireObj = (function() {
private$.loadedAsBrowserEsm = loadedAsBrowserEsm;
+ // Prevent monkey patching of existing properties but allow adding new ones.
+ // jasmine-html.js needs to be able to add to the jasmine namespace.
+ // jasmine-ajax also installs itself this way.
+ const writeable = [
+ 'DEFAULT_TIMEOUT_INTERVAL',
+ 'MAX_PRETTY_PRINT_ARRAY_LENGTH',
+ 'MAX_PRETTY_PRINT_CHARS',
+ 'MAX_PRETTY_PRINT_DEPTH'
+ ];
+ const descriptors = Object.getOwnPropertyDescriptors(j$);
+
+ for (const [k, d] of Object.entries(descriptors)) {
+ if (!writeable.includes(k)) {
+ Object.defineProperty(j$, k, {
+ value: d.value,
+ enumerable: d.enumerable,
+ configurable: false,
+ writable: false
+ });
+ }
+ }
+
return { jasmine: j$, private: private$ };
};
diff --git a/src/html/HtmlReporterV2.js b/src/html/HtmlReporterV2.js
index c0a0515d..5fa038e0 100644
--- a/src/html/HtmlReporterV2.js
+++ b/src/html/HtmlReporterV2.js
@@ -98,6 +98,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
);
this.#container.appendChild(this.#htmlReporterMain);
this.#failures.show();
+
+ Object.freeze(this);
}
jasmineStarted(options) {
@@ -275,5 +277,6 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
}
}
+ Object.freeze(HtmlReporterV2.prototype);
return HtmlReporterV2;
};
diff --git a/src/html/HtmlReporterV2Urls.js b/src/html/HtmlReporterV2Urls.js
index c6ef4825..41271170 100644
--- a/src/html/HtmlReporterV2Urls.js
+++ b/src/html/HtmlReporterV2Urls.js
@@ -18,6 +18,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
return window.location;
}
});
+
+ Object.freeze(this);
}
/**
@@ -68,5 +70,6 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
}
}
+ Object.freeze(HtmlReporterV2Urls.prototype);
return HtmlReporterV2Urls;
};
diff --git a/src/html/QueryString.js b/src/html/QueryString.js
index 9946c19a..7db5ba37 100644
--- a/src/html/QueryString.js
+++ b/src/html/QueryString.js
@@ -14,6 +14,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
*/
constructor(options) {
this.#getWindowLocation = options.getWindowLocation;
+ Object.freeze(this);
}
/**
@@ -81,5 +82,6 @@ getJasmineHtmlRequireObj().QueryString = function() {
return '?' + qStrPairs.join('&');
}
+ Object.freeze(QueryString.prototype);
return QueryString;
};
diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js
index 13ee40a2..fe99507c 100644
--- a/src/html/requireHtml.js
+++ b/src/html/requireHtml.js
@@ -25,10 +25,16 @@ const getJasmineHtmlRequireObj = (function() {
private$.FailuresView = htmlRequire.FailuresView(j$, private$);
private$.PerformanceView = htmlRequire.PerformanceView(j$, private$);
private$.TabBar = htmlRequire.TabBar(j$, private$);
- j$.HtmlReporterV2Urls = htmlRequire.HtmlReporterV2Urls(j$, private$);
- j$.HtmlReporterV2 = htmlRequire.HtmlReporterV2(j$, private$);
- j$.QueryString = htmlRequire.QueryString();
private$.HtmlSpecFilterV2 = htmlRequire.HtmlSpecFilterV2();
+
+ for (const k of ['HtmlReporterV2Urls', 'HtmlReporterV2', 'QueryString']) {
+ Object.defineProperty(j$, k, {
+ enumerable: true,
+ configurable: false,
+ writable: false,
+ value: htmlRequire[k](j$, private$)
+ });
+ }
};
return getJasmineHtmlRequire;