snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
873
web/tests/lib/zjquery_element.cjs
Normal file
873
web/tests/lib/zjquery_element.cjs
Normal file
@@ -0,0 +1,873 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const FakeEvent = require("./zjquery_event.cjs");
|
||||
|
||||
function split_words(x) {
|
||||
return Array.isArray(x) ? x : x.trim().split(/\s+/);
|
||||
}
|
||||
|
||||
const ignore_missing = Symbol("ignore_missing");
|
||||
|
||||
const reject_missing_handler = {
|
||||
has(target, property) {
|
||||
if (!(Reflect.has(target, property) || target[ignore_missing]?.(property))) {
|
||||
throw new TypeError(`unknown property ${property} of mock ${target.constructor.name}`);
|
||||
}
|
||||
return Reflect.has(target, property);
|
||||
},
|
||||
get(target, property, receiver) {
|
||||
if (!(Reflect.has(target, property) || target[ignore_missing]?.(property))) {
|
||||
throw new TypeError(`unknown property ${property} of mock ${target.constructor.name}`);
|
||||
}
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
ownKeys(target) {
|
||||
throw new TypeError(`enumerating properties of mock ${target.constructor.name}`);
|
||||
},
|
||||
};
|
||||
|
||||
class RejectMissing {
|
||||
constructor() {
|
||||
return new Proxy(this, reject_missing_handler);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeClassList extends RejectMissing {
|
||||
#list = new Set();
|
||||
|
||||
contains(class_name) {
|
||||
return this.#list.has(class_name);
|
||||
}
|
||||
add(...class_names) {
|
||||
for (const class_name of class_names) {
|
||||
this.#list.add(String(class_name));
|
||||
}
|
||||
}
|
||||
remove(...class_names) {
|
||||
for (const class_name of class_names) {
|
||||
this.#list.delete(String(class_name));
|
||||
}
|
||||
}
|
||||
toggle(class_name, state = !this.#list.has(String(class_name))) {
|
||||
if (state) {
|
||||
this.#list.add(String(class_name));
|
||||
} else {
|
||||
this.#list.delete(String(class_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeStyle extends RejectMissing {
|
||||
#style = new Map();
|
||||
|
||||
get length() {
|
||||
return this.#style.size;
|
||||
}
|
||||
getPropertyValue(name) {
|
||||
return this.#style.get(name) ?? "";
|
||||
}
|
||||
setProperty(name, value) {
|
||||
this.#style.set(name, String(value));
|
||||
}
|
||||
removeProperty(name) {
|
||||
const value = this.#style.get(name) ?? "";
|
||||
this.#style.delete(name);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeElementState {
|
||||
closest_results = new Map();
|
||||
computed_style = new FakeStyle();
|
||||
event_handlers = new Map();
|
||||
delegated_event_handlers = new Map();
|
||||
is_focused = false;
|
||||
jquery_data = new Map();
|
||||
match_results = new Map([["*", true]]);
|
||||
parents_results = new Map();
|
||||
query_results = new Map();
|
||||
selector = undefined;
|
||||
shown = false;
|
||||
}
|
||||
|
||||
const fake_element_state = new WeakMap();
|
||||
|
||||
function camel(s) {
|
||||
return s.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function decamel(s) {
|
||||
return s.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function normalize_attribute(name) {
|
||||
return String(name).replaceAll(/[A-Z]/g, (c) => c.toLowerCase());
|
||||
}
|
||||
|
||||
function attribute_to_dataset_key(name) {
|
||||
assert.ok(name.startsWith("data-"));
|
||||
return camel(name.slice("data-".length));
|
||||
}
|
||||
|
||||
function is_dataset_key(key) {
|
||||
return !/-[a-z]/.test(key);
|
||||
}
|
||||
|
||||
function dataset_key_to_attribute(key) {
|
||||
return `data-${decamel(key)}`;
|
||||
}
|
||||
|
||||
class FakeDataSet {
|
||||
constructor(element) {
|
||||
return new Proxy(this, {
|
||||
get(target, key, receiver) {
|
||||
return (
|
||||
(is_dataset_key(key)
|
||||
? element.getAttribute(dataset_key_to_attribute(key))
|
||||
: null) ?? Reflect.get(target, key, receiver)
|
||||
);
|
||||
},
|
||||
has(_target, key) {
|
||||
return is_dataset_key(key) && element.hasAttribute(dataset_key_to_attribute(key));
|
||||
},
|
||||
set(_target, key, value) {
|
||||
assert.ok(is_dataset_key(key));
|
||||
element.setAttribute(dataset_key_to_attribute(key), value);
|
||||
return true;
|
||||
},
|
||||
deleteProperty(_target, key) {
|
||||
if (is_dataset_key(key)) {
|
||||
element.removeAttribute(dataset_key_to_attribute(key));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
ownKeys(_target) {
|
||||
return element
|
||||
.getAttributeNames()
|
||||
.filter((name) => name.startsWith("data-"))
|
||||
.map((name) => attribute_to_dataset_key(name));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// https://api.jquery.com/css/
|
||||
const auto_px =
|
||||
/^(Border(Top|Right|Bottom|Left)?(Width)?|(Margin|Padding)?(Top|Right|Bottom|Left)?|(Min|Max)?(Width|Height))$/;
|
||||
|
||||
class FakeElement extends RejectMissing {
|
||||
_tippy = undefined;
|
||||
classList = new FakeClassList();
|
||||
dataset = new FakeDataSet(this);
|
||||
innerHTML = "never-been-set";
|
||||
selectionEnd = undefined;
|
||||
selectionStart = undefined;
|
||||
style = new FakeStyle();
|
||||
textContent = "never-been-set";
|
||||
value = undefined;
|
||||
|
||||
#attributes = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
fake_element_state.set(this, new FakeElementState());
|
||||
}
|
||||
append() {}
|
||||
closest(selector) {
|
||||
const state = fake_element_state.get(this);
|
||||
if (!state.closest_results.has(selector)) {
|
||||
throw new Error(
|
||||
`You need to call $(${JSON.stringify(state.selector)}).set_closest_results(${JSON.stringify(selector)}, ...)`,
|
||||
);
|
||||
}
|
||||
return state.closest_results.get(selector);
|
||||
}
|
||||
hasAttribute(name) {
|
||||
return this.#attributes.has(normalize_attribute(name));
|
||||
}
|
||||
getAttribute(name) {
|
||||
return this.#attributes.get(normalize_attribute(name)) ?? null;
|
||||
}
|
||||
getAttributeNames() {
|
||||
return this.#attributes.keys();
|
||||
}
|
||||
matches(selector) {
|
||||
const state = fake_element_state.get(this);
|
||||
if (!state.match_results.has(selector)) {
|
||||
throw new Error(
|
||||
`You need to call $(${JSON.stringify(state.selector)}).set_matches(${JSON.stringify(selector)}, boolean)`,
|
||||
);
|
||||
}
|
||||
return state.match_results.get(selector);
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
const state = fake_element_state.get(this);
|
||||
const results = state.query_results.get(selector);
|
||||
if (results) {
|
||||
return results;
|
||||
}
|
||||
throw new Error(
|
||||
`You need to call $(${JSON.stringify(state.selector)}).set_find_results(${JSON.stringify(selector)}, ...).`,
|
||||
);
|
||||
}
|
||||
removeAttribute(name) {
|
||||
this.#attributes.delete(normalize_attribute(name));
|
||||
}
|
||||
replaceChildren(...children) {
|
||||
assert.equal(children.length, 0, "zjquery does not support this replaceChildren() call");
|
||||
fake_element_state.get(this).query_results.clear();
|
||||
this.innerHTML = "";
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.#attributes.set(normalize_attribute(name), String(value));
|
||||
}
|
||||
setSelectionRange(start, end) {
|
||||
this.selectionStart = start;
|
||||
this.selectionEnd = end;
|
||||
}
|
||||
to_$() {
|
||||
return new exports.FakeJQuery([this]);
|
||||
}
|
||||
}
|
||||
|
||||
exports.default_element = function (selector) {
|
||||
const element = new FakeElement();
|
||||
fake_element_state.get(element).selector = selector;
|
||||
if (selector[0] === "<") {
|
||||
element.innerHTML = selector;
|
||||
}
|
||||
return element;
|
||||
};
|
||||
|
||||
function dom_args(args) {
|
||||
return args.flat().flatMap((arg) => {
|
||||
assert.equal(typeof arg, "object");
|
||||
return arg.__zjquery ? [...arg] : [arg];
|
||||
});
|
||||
}
|
||||
|
||||
exports.FakeJQuery = class extends RejectMissing {
|
||||
[Symbol.iterator] = Array.prototype.values;
|
||||
__zjquery = true;
|
||||
|
||||
constructor(elements) {
|
||||
super();
|
||||
this.length = elements.length;
|
||||
for (const [i, element] of elements.entries()) {
|
||||
this[i] = element;
|
||||
}
|
||||
}
|
||||
|
||||
get selector() {
|
||||
assert.equal(this.length, 1);
|
||||
return fake_element_state.get(this[0]).selector;
|
||||
}
|
||||
|
||||
addClass(class_names) {
|
||||
class_names = split_words(class_names);
|
||||
for (const element of this) {
|
||||
element.classList.add(...class_names);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
after(...args) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].after(...dom_args(args));
|
||||
return this;
|
||||
}
|
||||
append(...args) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].append(...dom_args(args));
|
||||
return this;
|
||||
}
|
||||
attr(name, ...args) {
|
||||
assert.notEqual(name, undefined);
|
||||
if (args.length === 0) {
|
||||
return 0 in this ? (this[0].getAttribute(name) ?? undefined) : undefined;
|
||||
}
|
||||
const [value] = args;
|
||||
for (const element of this) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
before(...args) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].before(...dom_args(args));
|
||||
return this;
|
||||
}
|
||||
caret(...args) {
|
||||
if (args.length === 0) {
|
||||
return this[0]?.selectionStart;
|
||||
}
|
||||
assert.equal(typeof args[0], "number", "zjquery does not support this caret() call");
|
||||
for (const element of this) {
|
||||
element.setSelectionRange(args[0], args[0]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
children(selector = "*") {
|
||||
return new exports.FakeJQuery(
|
||||
[...this].flatMap((element) =>
|
||||
[...element.children].filter((child) => child.matches(selector)),
|
||||
),
|
||||
);
|
||||
}
|
||||
closest(selector) {
|
||||
return new exports.FakeJQuery(
|
||||
[...this].flatMap((element) => element.closest(selector) ?? []),
|
||||
);
|
||||
}
|
||||
contents() {
|
||||
return new exports.FakeJQuery([...this].flatMap((element) => [...element.childNodes]));
|
||||
}
|
||||
css(property, ...args) {
|
||||
if (args.length === 0 && typeof property === "string") {
|
||||
if (!(0 in this)) {
|
||||
return undefined;
|
||||
}
|
||||
return fake_element_state
|
||||
.get(this[0])
|
||||
.computed_style.getPropertyValue(decamel(property));
|
||||
}
|
||||
|
||||
if (args.length === 0 && Array.isArray(property)) {
|
||||
if (!(0 in this)) {
|
||||
return undefined;
|
||||
}
|
||||
const state = fake_element_state.get(this[0]);
|
||||
return Object.fromEntries(
|
||||
property.map((key) => [key, state.computed_style.getPropertyValue(decamel(key))]),
|
||||
);
|
||||
}
|
||||
|
||||
for (const element of this) {
|
||||
const properties = typeof property === "string" ? {[property]: args[0]} : property;
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
element.style.setProperty(
|
||||
decamel(key),
|
||||
typeof value === "number" &&
|
||||
auto_px.test(camel(key).replace(/^./, (c) => c.toUpperCase()))
|
||||
? `${value}px`
|
||||
: value,
|
||||
);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
data(key, ...args) {
|
||||
if (args.length === 0) {
|
||||
if (!(0 in this)) {
|
||||
return undefined;
|
||||
}
|
||||
const state = fake_element_state.get(this[0]);
|
||||
if (state.jquery_data.has(key)) {
|
||||
return state.jquery_data.get(key);
|
||||
}
|
||||
let value = this[0].getAttribute(dataset_key_to_attribute(key));
|
||||
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^true$|^false$|^null$|^{.*}$|^\[.*]$/s.test(value)) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch {
|
||||
// use the unparsed value
|
||||
}
|
||||
} else if (Number(value).toString() === value) {
|
||||
value = Number(value);
|
||||
}
|
||||
|
||||
state.jquery_data.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const [value] = args;
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).jquery_data.set(key, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
detach() {
|
||||
for (const element of this) {
|
||||
element.remove();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
each(callback) {
|
||||
for (const [index, element] of [...this].entries()) {
|
||||
callback.call(element, index, element);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
empty() {
|
||||
for (const element of this) {
|
||||
element.replaceChildren();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
expectOne() {
|
||||
// silently do nothing
|
||||
return this;
|
||||
}
|
||||
fadeIn() {
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).shown = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
fadeOut(_duration, callback) {
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).shown = false;
|
||||
callback.call();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
fadeTo() {
|
||||
return this;
|
||||
}
|
||||
filter(arg) {
|
||||
return new exports.FakeJQuery(
|
||||
[...this].filter(
|
||||
typeof arg === "function"
|
||||
? (element, index) => arg.call(element, index, element)
|
||||
: (element) => element.matches(arg),
|
||||
),
|
||||
);
|
||||
}
|
||||
find(selector) {
|
||||
return new exports.FakeJQuery(
|
||||
[...this].flatMap((element) => [...element.querySelectorAll(selector)]),
|
||||
);
|
||||
}
|
||||
get(index) {
|
||||
return index === undefined ? [...this] : this[index];
|
||||
}
|
||||
get_on_handler(event_type, child_selector) {
|
||||
assert.ok(0 in this);
|
||||
const state = fake_element_state.get(this[0]);
|
||||
|
||||
if (child_selector === undefined) {
|
||||
const handler = state.event_handlers.get(event_type);
|
||||
assert.ok(handler !== undefined, `no ${event_type} handler for ${state.selector}`);
|
||||
return handler;
|
||||
}
|
||||
|
||||
const handler = state.delegated_event_handlers.get(child_selector)?.get(event_type);
|
||||
assert.ok(
|
||||
handler !== undefined,
|
||||
`no ${event_type} handler for ${state.selector} ${child_selector}`,
|
||||
);
|
||||
return handler;
|
||||
}
|
||||
hasClass(class_name) {
|
||||
return [...this].some((element) => element.classList.contains(class_name));
|
||||
}
|
||||
height(...args) {
|
||||
if (args.length === 0) {
|
||||
if (!(0 in this)) {
|
||||
return undefined;
|
||||
}
|
||||
const state = fake_element_state.get(this[0]);
|
||||
const height = state.computed_style.getPropertyValue("height");
|
||||
assert.notEqual(
|
||||
height,
|
||||
"",
|
||||
`Please call $(${JSON.stringify(state.selector)}).set_height`,
|
||||
);
|
||||
assert.ok(height.endsWith("px"));
|
||||
return Number(height.slice(0, -"px".length));
|
||||
}
|
||||
for (const element of this) {
|
||||
element.style.setProperty(
|
||||
"height",
|
||||
typeof args[0] === "number" ? `${args[0]}px` : args[0],
|
||||
);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
hide() {
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).shown = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
html(...args) {
|
||||
if (args.length === 0) {
|
||||
return this[0]?.innerHTML;
|
||||
}
|
||||
|
||||
const [arg] = args;
|
||||
assert.equal(typeof arg, "string");
|
||||
for (const element of this) {
|
||||
element.innerHTML = arg;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
insertAfter(...args) {
|
||||
args = dom_args(args);
|
||||
assert.equal(args.length, 1);
|
||||
args[0].after(...this);
|
||||
return this;
|
||||
}
|
||||
insertBefore(...args) {
|
||||
args = dom_args(args);
|
||||
assert.equal(args.length, 1);
|
||||
args[0].before(...this);
|
||||
return this;
|
||||
}
|
||||
is(arg) {
|
||||
switch (arg) {
|
||||
case ":visible":
|
||||
return [...this].some((element) => fake_element_state.get(element).shown);
|
||||
case ":focus":
|
||||
return this.is_focused();
|
||||
/* istanbul ignore next */
|
||||
default:
|
||||
return [...this].some((element) => element.matches(arg));
|
||||
}
|
||||
}
|
||||
is_focused() {
|
||||
// is_focused is not a jQuery thing; this is
|
||||
// for our testing
|
||||
assert.ok(0 in this);
|
||||
return fake_element_state.get(this[0]).is_focused;
|
||||
}
|
||||
last() {
|
||||
return new exports.FakeJQuery([...this].slice(-1));
|
||||
}
|
||||
next(selector = "*") {
|
||||
return new exports.FakeJQuery(
|
||||
[...this]
|
||||
.map((element) => element.nextElementSibling)
|
||||
.filter((next) => next !== null && next.matches(selector)),
|
||||
);
|
||||
}
|
||||
off(event_type, ...args) {
|
||||
if (args.length === 0) {
|
||||
for (const element of this) {
|
||||
const state = fake_element_state.get(element);
|
||||
state.event_handlers.delete(event_type);
|
||||
}
|
||||
} else {
|
||||
// In the Zulip codebase we never use this form of
|
||||
// .off in code that we test: $(...).off('click', child_sel);
|
||||
//
|
||||
// So we don't support this for now.
|
||||
/* istanbul ignore next */
|
||||
throw new Error("zjquery does not support this call sequence");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
on(event_type, ...args) {
|
||||
// parameters will either be
|
||||
// (event_type, handler) or
|
||||
// (event_type, sel, handler)
|
||||
if (args.length === 1) {
|
||||
const [handler] = args;
|
||||
for (const element of this) {
|
||||
const state = fake_element_state.get(element);
|
||||
/* istanbul ignore if */
|
||||
if (state.event_handlers.has(event_type)) {
|
||||
console.info("\nEither the app or the test can be at fault here..");
|
||||
console.info("(sometimes you just want to call $.clear_all_elements();)\n");
|
||||
throw new Error("dup " + event_type + " handler for " + state.selector);
|
||||
}
|
||||
|
||||
state.event_handlers.set(event_type, handler);
|
||||
}
|
||||
} else {
|
||||
assert.equal(args.length, 2, "wrong number of arguments passed in");
|
||||
|
||||
const [sel, handler] = args;
|
||||
assert.equal(typeof sel, "string", "String selectors expected here.");
|
||||
assert.equal(typeof handler, "function", "An handler function expected here.");
|
||||
|
||||
for (const element of this) {
|
||||
const state = fake_element_state.get(element);
|
||||
if (!state.delegated_event_handlers.has(sel)) {
|
||||
state.delegated_event_handlers.set(sel, new Map());
|
||||
}
|
||||
const child_on = state.delegated_event_handlers.get(sel);
|
||||
|
||||
assert.ok(
|
||||
!child_on.has(event_type),
|
||||
`dup ${event_type} handler for ${state.selector} ${sel}`,
|
||||
);
|
||||
|
||||
child_on.set(event_type, handler);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
one(event_type, handler) {
|
||||
return this.on(
|
||||
event_type,
|
||||
/* istanbul ignore next */ function (...args) {
|
||||
this.off(event_type);
|
||||
return handler.call(this, ...args);
|
||||
},
|
||||
);
|
||||
}
|
||||
outerHeight() {
|
||||
if (!(0 in this)) {
|
||||
return undefined;
|
||||
}
|
||||
const state = fake_element_state.get(this[0]);
|
||||
const height = state.computed_style.getPropertyValue("height");
|
||||
if (height === "") {
|
||||
return undefined;
|
||||
}
|
||||
assert.ok(height.endsWith("px"));
|
||||
return Number(height.slice(0, -2));
|
||||
}
|
||||
parent(selector = "*") {
|
||||
return new exports.FakeJQuery(
|
||||
[...this]
|
||||
.map((element) => element.parentNode)
|
||||
.filter((parent) => parent !== null && parent.matches(selector)),
|
||||
);
|
||||
}
|
||||
parents(selector = "*") {
|
||||
return new exports.FakeJQuery(
|
||||
[...this].flatMap((element) => {
|
||||
const state = fake_element_state.get(element);
|
||||
if (!state.parents_results.has(selector)) {
|
||||
throw new Error(
|
||||
`You need to call $(${JSON.stringify(state.selector)}).set_parents_result(${JSON.stringify(selector)}, ...)`,
|
||||
);
|
||||
}
|
||||
return state.parents_results.get(selector);
|
||||
}),
|
||||
);
|
||||
}
|
||||
prepend(...args) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].prepend(...dom_args(args));
|
||||
return this;
|
||||
}
|
||||
prev(selector = "*") {
|
||||
return new exports.FakeJQuery(
|
||||
[...this]
|
||||
.map((element) => element.previousElementSibling)
|
||||
.filter((prev) => prev !== null && prev.matches(selector)),
|
||||
);
|
||||
}
|
||||
prop(name, ...args) {
|
||||
if (args.length === 0) {
|
||||
return this[0]?.[name];
|
||||
}
|
||||
const [value] = args;
|
||||
for (const element of this) {
|
||||
element[name] = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
range(...args) {
|
||||
if (args.length === 0) {
|
||||
return 0 in this
|
||||
? {
|
||||
start: this[0].selectionStart,
|
||||
end: this[0].selectionEnd,
|
||||
length: this[0].selectionEnd - this[0].selectionStart,
|
||||
text: this[0].value.slice(this[0].selectionStart, this[0].selectionEnd),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
assert.equal(typeof args[0], "number", "zjquery does not support this range() call");
|
||||
for (const element of this) {
|
||||
element.setSelectionRange(args[0], args[1]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
removeAttr(name) {
|
||||
for (const element of this) {
|
||||
element.removeAttribute(name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
removeClass(class_names) {
|
||||
class_names = split_words(class_names);
|
||||
for (const element of this) {
|
||||
element.classList.remove(...class_names);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
remove() {
|
||||
for (const element of this) {
|
||||
element.remove();
|
||||
fake_element_state.get(element).jquery_data.clear();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
removeData(keys) {
|
||||
keys = split_words(keys);
|
||||
for (const element of this) {
|
||||
const state = fake_element_state.get(element);
|
||||
for (const key of keys) {
|
||||
state.jquery_data.delete(key);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
replaceWith(...args) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].replaceWith(...dom_args(args));
|
||||
return this;
|
||||
}
|
||||
set_children(elements) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].children = [...elements];
|
||||
}
|
||||
set_closest_results(selector, elements) {
|
||||
assert.equal(this.length, 1);
|
||||
fake_element_state.get(this[0]).closest_results.set(selector, elements[0] ?? null);
|
||||
}
|
||||
set_contents(nodes) {
|
||||
assert.equal(this.length, 1);
|
||||
this[0].childNodes = [...nodes];
|
||||
}
|
||||
set_find_results(selector, elements) {
|
||||
assert.equal(this.length, 1);
|
||||
fake_element_state.get(this[0]).query_results.set(selector, [...elements]);
|
||||
}
|
||||
set_height(fake_height) {
|
||||
for (const element of this) {
|
||||
fake_element_state
|
||||
.get(element)
|
||||
.computed_style.setProperty(
|
||||
"height",
|
||||
typeof fake_height === "number" ? `${fake_height}px` : fake_height,
|
||||
);
|
||||
}
|
||||
}
|
||||
set_matches(selector, value) {
|
||||
assert.equal(this.length, 1);
|
||||
fake_element_state.get(this[0]).match_results.set(selector, value);
|
||||
}
|
||||
set_next($result) {
|
||||
assert.equal(this.length, 1);
|
||||
assert.equal($result.length, 1);
|
||||
this[0].nextElementSibling = $result[0];
|
||||
}
|
||||
set_parent($parent_elem) {
|
||||
assert.equal(this.length, 1);
|
||||
assert.equal($parent_elem.length, 1);
|
||||
this[0].parentNode = $parent_elem[0];
|
||||
}
|
||||
set_parents_result(selector, elements) {
|
||||
assert.equal(this.length, 1);
|
||||
fake_element_state.get(this[0]).parents_results.set(selector, [...elements]);
|
||||
}
|
||||
set_prev($result) {
|
||||
assert.equal(this.length, 1);
|
||||
assert.equal($result.length, 1);
|
||||
this[0].previousElementSibling = $result[0];
|
||||
}
|
||||
show() {
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).shown = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
text(...args) {
|
||||
if (args.length === 0) {
|
||||
return [...this].map((element) => element.textContent).join("");
|
||||
}
|
||||
const [arg] = args;
|
||||
for (const [i, element] of [...this].entries()) {
|
||||
element.textContent =
|
||||
(typeof arg === "function"
|
||||
? arg.call(element, i, element.textContent)
|
||||
: arg
|
||||
)?.toString() ?? "";
|
||||
}
|
||||
return this;
|
||||
}
|
||||
// Used by zjquery to support $($x) === $x
|
||||
to_$() {
|
||||
return new exports.FakeJQuery([...this]);
|
||||
}
|
||||
toggle(show) {
|
||||
assert.ok([true, false].includes(show));
|
||||
for (const element of this) {
|
||||
fake_element_state.get(element).shown = show;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
toggleClass(class_names, add) {
|
||||
class_names = split_words(class_names);
|
||||
for (const element of this) {
|
||||
for (const class_name of class_names) {
|
||||
element.classList.toggle(class_name, add);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
trigger(event_arg, extra_args) {
|
||||
for (const element of this) {
|
||||
const event = new FakeEvent(
|
||||
typeof event_arg === "string" ? event_arg : event_arg.type,
|
||||
{
|
||||
target: element,
|
||||
currentTarget: element,
|
||||
...event_arg,
|
||||
},
|
||||
);
|
||||
const state = fake_element_state.get(element);
|
||||
const func = state.event_handlers.get(event.type);
|
||||
|
||||
if (func) {
|
||||
// It's possible that test code will trigger events
|
||||
// that haven't been set up yet, but we are trying to
|
||||
// eventually deprecate trigger in our codebase, so for
|
||||
// now we just let calls to trigger silently do nothing.
|
||||
// (And I think actual jQuery would do the same thing.)
|
||||
func.call(
|
||||
element,
|
||||
event,
|
||||
...(Array.isArray(extra_args) ? extra_args : [extra_args]),
|
||||
);
|
||||
}
|
||||
|
||||
if (event.type === "focus" || event.type === "focusin") {
|
||||
state.is_focused = true;
|
||||
} else if (event.type === "blur" || event.type === "focusout") {
|
||||
state.is_focused = false;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
unwrap(...args) {
|
||||
assert.equal(args.length, 0, "zjquery does not support this unwrap() call");
|
||||
for (const element of this) {
|
||||
element.parentNode.replaceWith(element.childNodes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
val(...args) {
|
||||
if (args.length === 0) {
|
||||
return 0 in this ? (this[0].value ?? "") : undefined;
|
||||
}
|
||||
const [value] = args;
|
||||
for (const element of this) {
|
||||
element.value = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
visible() {
|
||||
return [...this].some((element) => fake_element_state.get(element).shown);
|
||||
}
|
||||
[ignore_missing](property) {
|
||||
return [
|
||||
`${Number(property) >>> 0}`, // eslint-disable-line no-bitwise
|
||||
"__esModule",
|
||||
"stack",
|
||||
].includes(property);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user