zulip/frontend_tests/zjsunit/namespace.js

190 lines
5.8 KiB
JavaScript

"use strict";
const path = require("path");
const new_globals = new Set();
let old_globals = {};
exports.set_global = function (name, val) {
if (val === null) {
throw new Error(`
We try to avoid using null in our codebase.
`);
}
// Add this for debugging and to allow with_overrides
// to know that we're dealing with stubbed code.
if (typeof val === "object") {
val._patched_with_set_global = true;
}
if (!(name in old_globals)) {
if (!(name in global)) {
new_globals.add(name);
}
old_globals[name] = global[name];
}
global[name] = val;
return val;
};
function require_path(name, fn) {
if (fn === undefined) {
fn = "../../static/js/" + name;
} else if (/^generated\/|^js\/|^shared\/|^third\//.test(fn)) {
// FIXME: Stealing part of the NPM namespace is confusing.
fn = "../../static/" + fn;
}
return fn;
}
exports.zrequire = function (name, fn) {
return require(require_path(name, fn));
};
exports.reset_module = function (name, fn) {
fn = require_path(name, fn);
delete require.cache[require.resolve(fn)];
return require(fn);
};
const staticPath = path.resolve(__dirname, "../../static") + path.sep;
const templatesPath = staticPath + "templates" + path.sep;
exports.restore = function () {
for (const path of Object.keys(require.cache)) {
if (path.startsWith(staticPath) && !path.startsWith(templatesPath)) {
delete require.cache[path];
}
}
Object.assign(global, old_globals);
old_globals = {};
for (const name of new_globals) {
delete global[name];
}
new_globals.clear();
};
exports.with_field = function (obj, field, val, f) {
const had_val = Object.prototype.hasOwnProperty.call(obj, field);
const old_val = obj[field];
try {
obj[field] = val;
return f();
} finally {
if (had_val) {
obj[field] = old_val;
} else {
delete obj[field];
}
}
};
exports.with_overrides = function (test_function) {
// This function calls test_function() and passes in
// a way to override the namespace temporarily.
const restore_callbacks = [];
const unused_funcs = new Map();
const funcs = new Map();
const override = function (obj, func_name, f) {
// Given an object `obj` (which is usually a module object),
// we re-map `obj[func_name]` to the `f` passed in by the caller.
// Then the outer function here (`with_overrides`) automatically
// restores the original value of `obj[func_name]` as its last
// step. Generally our code calls `run_test`, which wraps
// `with_overrides`.
if (typeof f !== "function") {
throw new TypeError(
"You can only override with a function. Use with_field for non-functions.",
);
}
if (typeof obj !== "object" && typeof obj !== "function") {
throw new TypeError(`We cannot override a function for ${typeof obj} objects`);
}
if (obj[func_name] === undefined) {
if (obj !== global.$ && !obj._patched_with_set_global) {
throw new Error(`
It looks like you are overriding ${func_name}
for a module that never defined it, which probably
indicates that you have a typo or are doing
something hacky in the test.
`);
}
} else if (typeof obj[func_name] !== "function") {
throw new TypeError(`
You are overriding a non-function with a function.
This is almost certainly an error.
`);
}
if (!funcs.has(obj)) {
funcs.set(obj, new Map());
}
if (funcs.get(obj).has(func_name)) {
// Prevent overriding the same function twice, so that
// it's super easy to reason about our logic to restore
// the original function. Usually if somebody sees this
// error, it's a symptom of not breaking up tests enough.
throw new Error(
"You can only override a function one time. Use with_field for more granular control.",
);
}
funcs.get(obj).set(func_name, true);
if (!unused_funcs.has(obj)) {
unused_funcs.set(obj, new Map());
}
unused_funcs.get(obj).set(func_name, true);
let old_f = obj[func_name];
if (old_f === undefined) {
// Create a dummy function so that we can
// attach _patched_with_override to it.
old_f = () => {
throw new Error(`There is no ${func_name}() field for this object.`);
};
}
const new_f = function (...args) {
unused_funcs.get(obj).delete(func_name);
return f.apply(this, args);
};
// Let zjquery know this function was patched with override,
// so it doesn't complain about us modifying it. (Other
// code can also use this, as needed.)
new_f._patched_with_override = true;
obj[func_name] = new_f;
restore_callbacks.push(() => {
old_f._patched_with_override = true;
obj[func_name] = old_f;
delete old_f._patched_with_override;
});
};
try {
test_function(override);
} finally {
restore_callbacks.reverse();
for (const restore_callback of restore_callbacks) {
restore_callback();
}
}
for (const module_unused_funcs of unused_funcs.values()) {
for (const unused_name of module_unused_funcs.keys()) {
throw new Error(unused_name + " never got invoked!");
}
}
};