"use strict"; const {strict: assert} = require("assert"); /* When using zjquery, the first call to $("#foo") returns a new instance of the FakeElement pseudoclass, and then subsequent calls to $("#foo") get the same instance. */ const FakeElement = require("./zjquery_element"); const FakeEvent = require("./zjquery_event"); function verify_selector_for_zulip(selector) { const is_valid = "<#.".includes(selector[0]) || selector === "window-stub" || selector === "document-stub" || selector === "body" || selector === "html" || selector.location || selector.includes("#") || selector.includes(".") || (selector.includes("[") && selector.indexOf("]") >= selector.indexOf("[")); if (!is_valid) { // Check if selector has only english alphabets and space. // Then, the user is probably trying to use a tag as a selector // like $('div a'). if (/^[ A-Za-z]+$/.test(selector)) { throw new Error("Selector too broad! Use id, class or attributes of target instead."); } else { throw new Error("Invalid selector: " + selector + " Use $.create() maybe?"); } } } function make_zjquery() { const elems = new Map(); // Our fn structure helps us simulate extending jQuery. // Use this with extreme caution. const fn = {}; function new_elem(selector, create_opts) { const elem = FakeElement(selector, {...create_opts}); Object.assign(elem, fn); // Create a proxy handler to detect missing stubs. // // For context, zjquery doesn't implement every method/attribute // that you'd find on a "real" jQuery object. Sometimes we // expects devs to create their own stubs. const handler = { get: (target, key) => { // Handle the special case of equality checks, which // we can infer by assert.equal trying to access the // "stack" key. if (key === "stack") { const error = "\nInstead of doing equality checks on a full object, " + 'do `assert_equal(foo.selector, ".some_class")\n'; throw new Error(error); } const val = target[key]; if (val === undefined && typeof key !== "symbol" && key !== "inspect") { // For undefined values, we'll throw errors to devs saying // they need to create stubs. We ignore certain keys that // are used for simply printing out the object. throw new Error('You must create a stub for $("' + selector + '").' + key); } return val; }, }; const proxy = new Proxy(elem, handler); return proxy; } let initialize_function; const zjquery = function (arg, arg2) { if (typeof arg === "function") { if (initialize_function) { throw new Error(` We are trying to avoid the $(...) mechanism for initializing modules in our codebase, and the code that you are compiling/running has tried to do this twice. Please either clean up the real code or reduce the scope of what you are testing in this test module. `); } initialize_function = arg; return undefined; } // If somebody is passing us an element, we return // the element itself if it's been created with // zjquery. // This may happen in cases like $(this). if (arg.selector && elems.has(arg.selector)) { return arg; } // We occasionally create stub objects that know // they want to be wrapped by jQuery (so they can // in turn return stubs). The convention is that // they provide a to_$ attribute. if (arg.to_$) { assert.equal(typeof arg.to_$, "function"); return arg.to_$(); } if (arg2 !== undefined) { throw new Error("We only use one-argument variations of $(...) in Zulip code."); } const selector = arg; if (typeof selector !== "string") { console.info(arg); throw new Error("zjquery does not know how to wrap this object yet"); } verify_selector_for_zulip(selector); if (!elems.has(selector)) { const elem = new_elem(selector); elems.set(selector, elem); } return elems.get(selector); }; zjquery.get_initialize_function = function () { return initialize_function; }; zjquery.clear_initialize_function = function () { initialize_function = undefined; }; zjquery.create = function (name, opts) { assert.ok(!elems.has(name), "You already created an object with this name!!"); const elem = new_elem(name, opts); elems.set(name, elem); return elem; }; zjquery.trim = function (s) { return s; }; zjquery.state = function () { // useful for debugging let res = Array.from(elems.values(), (v) => v.debug()); res = res.map((v) => [v.selector, v.value, v.shown]); res.sort(); return res; }; zjquery.Event = FakeEvent; fn.popover = () => { throw new Error(` Do not try to test $.fn.popover code unless you really know what you are doing. `); }; zjquery.fn = new Proxy(fn, { set(obj, prop, value) { if (prop === "popover") { // We allow our popovers test to modify // $.fn so we can bypass a gruesome hack // in our popovers.js module. obj[prop] = value; return true; } throw new Error(` Please don't use node tests to test code that extends $.fn unless you really know what you are doing. It's likely that you are better off testing end-to-end behavior with puppeteer tests. If you are trying to get coverage on a module that extends $.fn, and you just want to skip over that aspect of the module for the purpose of testing, see if you can wrap the code that extends $.fn and use override() to replace the wrapper with () => {}. `); }, }); zjquery.clear_all_elements = function () { elems.clear(); }; zjquery.validator = { addMethod() { throw new Error("You must create your own $.validator.addMethod stub."); }, }; return zjquery; } const $ = new Proxy(make_zjquery(), { set(obj, prop, value) { if (obj[prop] && obj[prop]._patched_with_override) { obj[prop] = value; return true; } if (value._patched_with_override) { obj[prop] = value; return true; } throw new Error(` Please don't modify $.${prop} if you are using zjquery. You can do this instead: override($, "${prop}", () => {...}); Or you can do this if you don't actually need zjquery and just want to simulate one function. mock_cjs("jquery", { ${prop}(...) {...}, }); It's also possible that you are testing code with node tests when it would be a better strategy to use puppeteer tests. `); }, }); module.exports = $;