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:
48
web/tests/lib/buddy_list.cjs
Normal file
48
web/tests/lib/buddy_list.cjs
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
|
||||
const {$} = require("./zjquery.cjs");
|
||||
|
||||
let users_matching_view = [];
|
||||
exports.buddy_list_add_user_matching_view = (user_id, $stub) => {
|
||||
if ($stub.attr) {
|
||||
$stub.attr("data-user-id", user_id);
|
||||
}
|
||||
users_matching_view.push(user_id);
|
||||
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
|
||||
$("#buddy_list_wrapper").set_find_results(sel, $stub);
|
||||
};
|
||||
|
||||
let other_users = [];
|
||||
exports.buddy_list_add_other_user = (user_id, $stub) => {
|
||||
if ($stub.attr) {
|
||||
$stub.attr("data-user-id", user_id);
|
||||
}
|
||||
other_users.push(user_id);
|
||||
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
|
||||
$("#buddy_list_wrapper").set_find_results(sel, $stub);
|
||||
};
|
||||
|
||||
exports.override_user_matches_narrow_using_loaded_data = (user_id) =>
|
||||
users_matching_view.includes(user_id);
|
||||
|
||||
exports.clear_buddy_list = (buddy_list) => {
|
||||
buddy_list.populate({
|
||||
all_user_ids: [],
|
||||
});
|
||||
users_matching_view = [];
|
||||
other_users = [];
|
||||
};
|
||||
|
||||
exports.stub_buddy_list_elements = () => {
|
||||
// Set to an empty list since we're not testing CSS.
|
||||
$.reset_selector("#buddy-list-users-matching-view .empty-list-message");
|
||||
$.set_results("#buddy-list-users-matching-view .empty-list-message", []);
|
||||
$.reset_selector("#buddy-list-other-users .empty-list-message");
|
||||
$.set_results("#buddy-list-other-users .empty-list-message", []);
|
||||
$.reset_selector("#buddy-list-other-users-container .view-all-users-link");
|
||||
$.set_results("#buddy-list-other-users-container .view-all-users-link", []);
|
||||
|
||||
// Simulate no avatar images for clear_avatar_preload_backgrounds.
|
||||
$.reset_selector("#user-list .avatar-preload-background img");
|
||||
$.set_results("#user-list .avatar-preload-background img", []);
|
||||
};
|
||||
43
web/tests/lib/compose_banner.cjs
Normal file
43
web/tests/lib/compose_banner.cjs
Normal file
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
const compose_banner = require("../../src/compose_banner.ts");
|
||||
|
||||
const {noop} = require("./test.cjs");
|
||||
const {$} = require("./zjquery.cjs");
|
||||
|
||||
exports.mock_banners = () => {
|
||||
// zjquery doesn't support `remove`, which is used when clearing the compose box.
|
||||
// TODO: improve how we test this so that we don't have to mock things like this.
|
||||
for (const classname of Object.values(compose_banner.CLASSNAMES)) {
|
||||
$(
|
||||
`#compose_banners .${classname
|
||||
.split(" ")
|
||||
.map((classname) => CSS.escape(classname))
|
||||
.join(".")}`,
|
||||
)[0].remove = noop;
|
||||
}
|
||||
$("#compose_banners .warning")[0].remove = noop;
|
||||
$("#compose_banners .error")[0].remove = noop;
|
||||
$("#compose_banners .error:not(.upload_banner)")[0].remove = noop;
|
||||
$("#compose_banners .upload_banner")[0].remove = noop;
|
||||
$("#compose_banners .upload_banner.error")[0].remove = noop;
|
||||
|
||||
const $stub = $.set_results("stub_to_remove", []);
|
||||
const $cb = $("#compose_banners");
|
||||
|
||||
$cb.set_closest_results(".edit_form_banners", $.create("edit-form-banners-stub"));
|
||||
$cb.set_find_results(".upload_banner.error", $stub);
|
||||
$cb.set_find_results(".no_post_permissions", $stub);
|
||||
$cb.set_find_results(".message_too_long", $stub);
|
||||
$cb.set_find_results(".wildcards_not_allowed", $stub);
|
||||
$cb.set_find_results(".wildcard_warning", $stub);
|
||||
$cb.set_find_results(".topic_missing", $stub);
|
||||
$cb.set_find_results(".missing_stream", $stub);
|
||||
$cb.set_find_results(".deactivated_user", $stub);
|
||||
$cb.set_find_results(".missing_private_message_recipient", $stub);
|
||||
$cb.set_find_results(".subscription_error", $stub);
|
||||
$cb.set_find_results(".generic_compose_error", $stub);
|
||||
$cb.set_find_results(".topic_resolved", $stub);
|
||||
$cb.set_find_results(".search_view", $stub);
|
||||
$cb.set_find_results(".unscheduled_message", $stub);
|
||||
};
|
||||
217
web/tests/lib/compose_helpers.cjs
Normal file
217
web/tests/lib/compose_helpers.cjs
Normal file
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {$} = require("./zjquery.cjs");
|
||||
|
||||
class FakeComposeBox {
|
||||
constructor() {
|
||||
this.$send_message_form = $("#send_message_form");
|
||||
this.$content_textarea = $("textarea#compose-textarea");
|
||||
this.$preview_message_area = $("#compose .preview_message_area");
|
||||
|
||||
// Simulate DOM relationships
|
||||
this.$send_message_form.set_find_results(".message-textarea", this.$content_textarea);
|
||||
|
||||
// These are needed by compose_ui.enter_preview_mode() and
|
||||
// compose_ui.exit_preview_mode(), which use $container.find().
|
||||
const $compose = $("#compose");
|
||||
$compose.set_find_results("textarea.message-textarea", this.$content_textarea);
|
||||
$compose.set_find_results(
|
||||
".preview_mode_disabled .compose_control_button",
|
||||
$("#compose .preview_mode_disabled .compose_control_button"),
|
||||
);
|
||||
$compose.set_find_results(".markdown_preview", $("#compose .markdown_preview"));
|
||||
$compose.set_find_results(".undo_markdown_preview", $("#compose .undo_markdown_preview"));
|
||||
$compose.set_find_results(".preview_message_area", this.$preview_message_area);
|
||||
$compose.set_find_results(".preview_content", $("#compose .preview_content"));
|
||||
|
||||
this.$send_message_form.set_find_results(
|
||||
".message-limit-indicator",
|
||||
$(".message-limit-indicator"),
|
||||
);
|
||||
|
||||
const $message_row_stub = $.set_results("message_row_stub", []);
|
||||
this.$content_textarea.set_closest_results(".message_row", $message_row_stub);
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
$(".message-limit-indicator").html("");
|
||||
$(".message-limit-indicator").text("");
|
||||
|
||||
$.reset_selector("#compose_banners .user_not_subscribed");
|
||||
$.set_results("#compose_banners .user_not_subscribed", []);
|
||||
|
||||
this.$content_textarea.set_height(50);
|
||||
this.$content_textarea.val("default message");
|
||||
this.$content_textarea.trigger("blur");
|
||||
|
||||
$(".compose-submit-button .loader").show();
|
||||
}
|
||||
|
||||
show_message_preview() {
|
||||
this.$preview_message_area.show();
|
||||
$("#compose .undo_markdown_preview").show();
|
||||
$("#compose .markdown_preview").hide();
|
||||
$("#compose").addClass("preview_mode");
|
||||
}
|
||||
|
||||
hide_message_preview() {
|
||||
this.$preview_message_area.hide();
|
||||
$("#compose .markdown_preview").show();
|
||||
$("#compose .undo_markdown_preview").hide();
|
||||
$("#compose").removeClass("preview_mode");
|
||||
}
|
||||
|
||||
textarea_val() {
|
||||
return this.$content_textarea.val();
|
||||
}
|
||||
|
||||
preview_content_html() {
|
||||
return $("#compose .preview_content").html();
|
||||
}
|
||||
|
||||
compose_spinner_selector() {
|
||||
return ".compose-submit-button .loader";
|
||||
}
|
||||
|
||||
markdown_spinner_selector() {
|
||||
return "#compose .markdown_preview_spinner";
|
||||
}
|
||||
|
||||
set_topic_val(topic_name) {
|
||||
$("input#stream_message_recipient_topic").val(topic_name);
|
||||
}
|
||||
|
||||
set_textarea_val(val) {
|
||||
this.$content_textarea.val(val);
|
||||
}
|
||||
|
||||
blur_textarea() {
|
||||
this.$content_textarea.trigger("blur");
|
||||
}
|
||||
|
||||
show_submit_button_spinner() {
|
||||
$(".compose-submit-button .loader").show();
|
||||
}
|
||||
|
||||
is_recipient_not_subscribed_banner_visible() {
|
||||
return $("#compose_banners .recipient_not_subscribed").visible();
|
||||
}
|
||||
|
||||
is_textarea_focused() {
|
||||
return this.$content_textarea.is_focused();
|
||||
}
|
||||
|
||||
is_submit_button_spinner_visible() {
|
||||
return $(this.compose_spinner_selector()).visible();
|
||||
}
|
||||
|
||||
trigger_submit_handler_on_compose_form(event) {
|
||||
$("#compose form").get_on_handler("submit")(event);
|
||||
}
|
||||
|
||||
click_on_markdown_preview_icon(event) {
|
||||
$("#compose").get_on_handler("click", ".markdown_preview")(event);
|
||||
}
|
||||
|
||||
click_on_undo_markdown_preview_icon(event) {
|
||||
$("#compose").get_on_handler("click", ".undo_markdown_preview")(event);
|
||||
}
|
||||
|
||||
click_on_upload_attachment_icon(event) {
|
||||
$("#compose").get_on_handler("click", ".compose_upload_file")(event);
|
||||
}
|
||||
|
||||
set_click_handler_for_upload_file_input_element(f) {
|
||||
$("#compose .file_input").on("click", f);
|
||||
}
|
||||
|
||||
assert_preview_mode_is_off() {
|
||||
assert.ok(!this.$preview_message_area.visible());
|
||||
assert.ok(!$("#compose .undo_markdown_preview").visible());
|
||||
assert.ok($("#compose .markdown_preview").visible());
|
||||
assert.ok(!$("#compose").hasClass("preview_mode"));
|
||||
}
|
||||
|
||||
assert_preview_mode_is_on() {
|
||||
assert.ok(this.$preview_message_area.visible());
|
||||
assert.ok(!$("#compose .markdown_preview").visible());
|
||||
assert.ok($("#compose .undo_markdown_preview").visible());
|
||||
assert.ok($("#compose").hasClass("preview_mode"));
|
||||
}
|
||||
|
||||
assert_message_size_is_over_the_limit(desired_html) {
|
||||
// Indicator should show red colored text
|
||||
assert.equal($(".message-limit-indicator").html(), desired_html);
|
||||
|
||||
assert.ok(this.$content_textarea.hasClass("textarea-over-limit"));
|
||||
assert.ok($(".message-limit-indicator").hasClass("textarea-over-limit"));
|
||||
assert.ok(!$("#compose-send-button").hasClass("disabled-message-send-controls"));
|
||||
}
|
||||
|
||||
assert_message_size_is_under_the_limit(desired_html) {
|
||||
// Work around the quirk that our validation code
|
||||
// arbitrarily switches between html() and text(),
|
||||
// and zjquery doesn't unify text and html.
|
||||
if (desired_html) {
|
||||
assert.equal($(".message-limit-indicator").html(), desired_html);
|
||||
} else {
|
||||
assert.equal($(".message-limit-indicator").text(), "");
|
||||
}
|
||||
|
||||
assert.ok(!this.$content_textarea.hasClass("textarea-over-limit"));
|
||||
assert.ok(!$(".message-limit-indicator").hasClass("textarea-over-limit"));
|
||||
assert.ok(!$("#compose-send-button").hasClass("disabled-message-send-controls"));
|
||||
}
|
||||
}
|
||||
|
||||
function quote_message_template(opts) {
|
||||
const {channel_object, selected_message, fence, content} = opts;
|
||||
|
||||
const {stream_id, name} = channel_object;
|
||||
const channel_name = name;
|
||||
const {sender_full_name, sender_id, id, topic} = selected_message;
|
||||
const near_url = `http://zulip.zulipdev.com/#narrow/channel/${stream_id}-${channel_name}/topic/${topic}/near/${id}`;
|
||||
return `translated: @_**${sender_full_name}|${sender_id}** [said](${near_url}):
|
||||
${fence}quote
|
||||
${content}
|
||||
${fence}`;
|
||||
}
|
||||
|
||||
function forward_channel_message_template(opts) {
|
||||
const {channel_object, selected_message, fence, content} = opts;
|
||||
|
||||
const {stream_id, name} = channel_object;
|
||||
const channel_name = name;
|
||||
const {sender_full_name, sender_id, id, topic} = selected_message;
|
||||
const near_url = `http://zulip.zulipdev.com/#narrow/channel/${stream_id}-${channel_name}/topic/${topic}/near/${id}`;
|
||||
const topic_link = `#**${channel_name}>${topic}**`;
|
||||
return `translated: @_**${sender_full_name}|${sender_id}** [said](${near_url}) in ${topic_link}:
|
||||
${fence}quote
|
||||
${content}
|
||||
${fence}`;
|
||||
}
|
||||
|
||||
function forward_direct_message_template(opts) {
|
||||
const {direct_message, dm_recipient_string, fence, content} = opts;
|
||||
|
||||
const {sender_full_name, sender_id, id: message_id, display_recipient} = direct_message;
|
||||
const encoding_prefix = display_recipient.length > 2 ? "group" : "dm";
|
||||
const dm_recipient_ids_string = display_recipient.map((user) => user.id).join(",");
|
||||
const near_url = `http://zulip.zulipdev.com/#narrow/dm/${dm_recipient_ids_string}-${encoding_prefix}/near/${message_id}`;
|
||||
|
||||
return `translated: @_**${sender_full_name}|${sender_id}** [said](${near_url}) to ${dm_recipient_string}:
|
||||
${fence}quote
|
||||
${content}
|
||||
${fence}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FakeComposeBox,
|
||||
quote_message_template,
|
||||
forward_channel_message_template,
|
||||
forward_direct_message_template,
|
||||
};
|
||||
1395
web/tests/lib/events.cjs
Normal file
1395
web/tests/lib/events.cjs
Normal file
File diff suppressed because it is too large
Load Diff
31
web/tests/lib/example_group.cjs
Normal file
31
web/tests/lib/example_group.cjs
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
let last_issued_group_id = 20000;
|
||||
|
||||
const get_group_id = () => {
|
||||
last_issued_group_id += 1 + Math.floor(Math.random() * 10);
|
||||
return last_issued_group_id;
|
||||
};
|
||||
|
||||
exports.make_user_group = (opts = {}) => {
|
||||
const id = opts.id ?? get_group_id();
|
||||
|
||||
return {
|
||||
description: "Dummy user group",
|
||||
id,
|
||||
creator_id: 1,
|
||||
date_created: null,
|
||||
name: "Example user group",
|
||||
members: [],
|
||||
is_system_group: false,
|
||||
direct_subgroup_ids: [],
|
||||
can_add_members_group: 0,
|
||||
can_join_group: 0,
|
||||
can_leave_group: 0,
|
||||
can_manage_group: 0,
|
||||
can_mention_group: 0,
|
||||
can_remove_members_group: 0,
|
||||
deactivated: false,
|
||||
...opts,
|
||||
};
|
||||
};
|
||||
161
web/tests/lib/example_realm.cjs
Normal file
161
web/tests/lib/example_realm.cjs
Normal file
@@ -0,0 +1,161 @@
|
||||
"use strict";
|
||||
|
||||
const {server_supported_permission_settings} = require("./example_settings.cjs");
|
||||
|
||||
exports.make_realm = (opts = {}) => {
|
||||
const default_realm = {
|
||||
custom_profile_fields: [],
|
||||
custom_profile_field_types: [],
|
||||
giphy_api_key: "giphy-api-key",
|
||||
gif_rating_policy_options: {disabled: {id: 0, name: ""}},
|
||||
max_avatar_file_size_mib: 0,
|
||||
max_channel_folder_description_length: 0,
|
||||
max_channel_folder_name_length: 0,
|
||||
max_file_upload_size_mib: 0,
|
||||
max_icon_file_size_mib: 0,
|
||||
max_logo_file_size_mib: 0,
|
||||
max_message_length: 100,
|
||||
max_reminder_note_length: 0,
|
||||
max_stream_description_length: 0,
|
||||
max_stream_name_length: 0,
|
||||
max_topic_length: 0,
|
||||
max_bulk_new_subscription_messages: 0,
|
||||
password_min_guesses: 0,
|
||||
password_min_length: 0,
|
||||
password_max_length: 0,
|
||||
realm_allow_message_editing: false,
|
||||
realm_authentication_methods: {
|
||||
default: {
|
||||
enabled: false,
|
||||
available: false,
|
||||
unavailable_reason: "",
|
||||
},
|
||||
},
|
||||
realm_available_video_chat_providers_default: {
|
||||
disabled: {name: "", id: 0},
|
||||
jitsi_meet: {name: "", id: 0},
|
||||
zoom: undefined,
|
||||
zoom_server_to_server: undefined,
|
||||
big_blue_button: undefined,
|
||||
},
|
||||
realm_avatar_changes_disabled: false,
|
||||
realm_bot_domain: "",
|
||||
realm_can_access_all_users_group: 0,
|
||||
realm_can_add_custom_emoji_group: 0,
|
||||
realm_can_add_subscribers_group: 0,
|
||||
realm_can_create_bots_group: 0,
|
||||
realm_can_create_groups: 0,
|
||||
realm_can_create_public_channel_group: 0,
|
||||
realm_can_create_private_channel_group: 0,
|
||||
realm_can_create_web_public_channel_group: 0,
|
||||
realm_can_create_write_only_bots_group: 0,
|
||||
realm_can_delete_any_message_group: {
|
||||
direct_subgroups: [],
|
||||
direct_members: [],
|
||||
},
|
||||
realm_can_delete_own_message_group: 0,
|
||||
realm_can_invite_users_group: 0,
|
||||
realm_can_manage_all_groups: 0,
|
||||
realm_can_manage_billing_group: 0,
|
||||
realm_can_mention_many_users_group: 0,
|
||||
realm_can_move_messages_between_channels_group: 0,
|
||||
realm_can_move_messages_between_topics_group: 0,
|
||||
realm_can_resolve_topics_group: 0,
|
||||
realm_can_set_delete_message_policy_group: 0,
|
||||
realm_can_set_topics_policy_group: 0,
|
||||
realm_can_summarize_topics_group: 0,
|
||||
realm_create_multiuse_invite_group: 0,
|
||||
realm_date_created: 0,
|
||||
realm_default_code_block_language: "",
|
||||
realm_default_external_accounts: {},
|
||||
realm_default_language: "",
|
||||
realm_description: "Dummy realm",
|
||||
realm_digest_emails_enabled: false,
|
||||
realm_digest_weekday: 0,
|
||||
realm_direct_message_initiator_group: 0,
|
||||
realm_direct_message_permission_group: 0,
|
||||
realm_disallow_disposable_email_addresses: false,
|
||||
realm_domains: [],
|
||||
realm_email_auth_enabled: false,
|
||||
realm_email_changes_disabled: false,
|
||||
realm_emails_restricted_to_domains: false,
|
||||
realm_embedded_bots: [],
|
||||
realm_empty_topic_display_name: "",
|
||||
realm_enable_guest_user_dm_warning: false,
|
||||
realm_enable_guest_user_indicator: false,
|
||||
realm_enable_read_receipts: false,
|
||||
realm_enable_spectator_access: false,
|
||||
realm_gif_rating_policy: 0,
|
||||
realm_icon_source: "",
|
||||
realm_icon_url: "",
|
||||
realm_incoming_webhook_bots: [],
|
||||
realm_media_preview_size: 100,
|
||||
realm_inline_image_preview: false,
|
||||
realm_inline_url_embed_preview: false,
|
||||
realm_invite_required: false,
|
||||
realm_jitsi_server_url: null,
|
||||
realm_linkifiers: [],
|
||||
realm_logo_source: "",
|
||||
realm_logo_url: "",
|
||||
realm_message_content_allowed_in_email_notifications: false,
|
||||
realm_message_content_edit_limit_seconds: null,
|
||||
realm_message_content_delete_limit_seconds: null,
|
||||
realm_message_edit_history_visibility_policy: "all",
|
||||
realm_message_retention_days: 0,
|
||||
realm_move_messages_between_streams_limit_seconds: null,
|
||||
realm_move_messages_within_stream_limit_seconds: null,
|
||||
realm_name_changes_disabled: false,
|
||||
realm_name: "",
|
||||
realm_new_stream_announcements_stream_id: 0,
|
||||
realm_night_logo_source: "",
|
||||
realm_night_logo_url: "",
|
||||
realm_org_type: 0,
|
||||
realm_password_auth_enabled: false,
|
||||
realm_plan_type: 0,
|
||||
realm_playgrounds: [],
|
||||
realm_presence_disabled: false,
|
||||
realm_push_notifications_enabled: false,
|
||||
realm_push_notifications_enabled_end_timestamp: null,
|
||||
realm_require_e2ee_push_notifications: false,
|
||||
realm_require_unique_names: false,
|
||||
realm_send_welcome_emails: false,
|
||||
realm_signup_announcements_stream_id: 0,
|
||||
realm_topics_policy: "allow_empty_topic",
|
||||
realm_upload_quota_mib: null,
|
||||
realm_url: "",
|
||||
realm_video_chat_provider: 0,
|
||||
realm_waiting_period_threshold: 0,
|
||||
realm_want_advertise_in_communities_directory: false,
|
||||
realm_welcome_message_custom_text: "",
|
||||
realm_zulip_update_announcements_stream_id: 0,
|
||||
server_avatar_changes_disabled: false,
|
||||
server_can_summarize_topics: false,
|
||||
server_emoji_data_url: "",
|
||||
server_inline_image_preview: false,
|
||||
server_inline_url_embed_preview: false,
|
||||
server_max_deactivated_realm_deletion_days: null,
|
||||
server_min_deactivated_realm_deletion_days: null,
|
||||
server_jitsi_server_url: null,
|
||||
server_name_changes_disabled: false,
|
||||
server_needs_upgrade: false,
|
||||
server_presence_offline_threshold_seconds: 0,
|
||||
server_presence_ping_interval_seconds: 0,
|
||||
server_supported_permission_settings,
|
||||
server_thumbnail_formats: [],
|
||||
server_typing_started_expiry_period_milliseconds: 0,
|
||||
server_typing_started_wait_period_milliseconds: 0,
|
||||
server_typing_stopped_wait_period_milliseconds: 0,
|
||||
server_web_public_streams_enabled: false,
|
||||
settings_send_digest_emails: false,
|
||||
stop_words: [],
|
||||
upgrade_text_for_wide_organization_logo: "",
|
||||
zulip_feature_level: 0,
|
||||
zulip_merge_base: "",
|
||||
zulip_plan_is_not_limited: false,
|
||||
zulip_version: "11",
|
||||
};
|
||||
return {
|
||||
...default_realm,
|
||||
...opts,
|
||||
};
|
||||
};
|
||||
384
web/tests/lib/example_settings.cjs
Normal file
384
web/tests/lib/example_settings.cjs
Normal file
@@ -0,0 +1,384 @@
|
||||
"use strict";
|
||||
|
||||
exports.server_supported_permission_settings = {
|
||||
stream: {
|
||||
can_add_subscribers_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_administer_channel_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "channel_creator",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_topic_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_delete_any_message_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_delete_own_message_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_move_messages_out_of_channel_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_move_messages_within_channel_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_remove_subscribers_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: false,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:administrators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_resolve_topics_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_send_message_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_subscribe_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
},
|
||||
realm: {
|
||||
can_access_all_users_group: {
|
||||
require_system_group: true,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: false,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: ["role:everyone", "role:members"],
|
||||
},
|
||||
can_add_custom_emoji_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_add_subscribers_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_bots_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_groups: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_private_channel_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_public_channel_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_create_web_public_channel_group: {
|
||||
require_system_group: true,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:owners",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [
|
||||
"role:moderators",
|
||||
"role:administrators",
|
||||
"role:owners",
|
||||
"role:nobody",
|
||||
],
|
||||
},
|
||||
can_create_write_only_bots_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_delete_any_message_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:administrators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_delete_own_message_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_invite_users_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_manage_all_groups: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: false,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:owners",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_manage_billing_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: false,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:administrators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_mention_many_users_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:administrators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_move_messages_between_channels_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_move_messages_between_topics_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_resolve_topics_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_set_delete_message_policy_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:moderators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_set_topics_policy_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:members",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_summarize_topics_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
create_multiuse_invite_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:administrators",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
direct_message_initiator_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
direct_message_permission_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: null,
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
},
|
||||
group: {
|
||||
can_add_members_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "group_creator",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_join_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_leave_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_manage_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "group_creator",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_mention_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: true,
|
||||
default_group_name: "role:everyone",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
can_remove_members_group: {
|
||||
require_system_group: false,
|
||||
allow_internet_group: false,
|
||||
allow_nobody_group: true,
|
||||
allow_everyone_group: false,
|
||||
default_group_name: "role:nobody",
|
||||
default_for_system_groups: "role:nobody",
|
||||
allowed_system_groups: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
57
web/tests/lib/example_stream.cjs
Normal file
57
web/tests/lib/example_stream.cjs
Normal file
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
let last_issued_stream_id = 20000;
|
||||
|
||||
const get_stream_id = () => {
|
||||
last_issued_stream_id += 1 + Math.floor(Math.random() * 10);
|
||||
return last_issued_stream_id;
|
||||
};
|
||||
|
||||
exports.make_stream = (opts = {}) => {
|
||||
// Since other fields are computed from these, we need to
|
||||
// pull these out of opts early.
|
||||
const stream_id = opts.stream_id ?? get_stream_id();
|
||||
const name = opts.name ?? `stream-${stream_id}`;
|
||||
|
||||
const default_channel = {
|
||||
audible_notifications: false,
|
||||
/* BUG: This should always be a group ID. But it's annoying to
|
||||
* fix without assuming groups exist in the data set. */
|
||||
can_remove_subscribers_group: 0,
|
||||
can_administer_channel_group: 2,
|
||||
can_create_topic_group: 2,
|
||||
color: "abcd12",
|
||||
/* This is rarely going to be the case, but a valid possibility. */
|
||||
creator_id: null,
|
||||
date_created: Date.now(),
|
||||
description: `Description of ${name}`,
|
||||
desktop_notifications: false,
|
||||
email_address: "channel-email-address@example.com",
|
||||
email_notifications: false,
|
||||
/* This will rarely be the case, but is a valid possibility*/
|
||||
first_message_id: null,
|
||||
history_public_to_subscribers: true,
|
||||
invite_only: false,
|
||||
is_announcement_only: false,
|
||||
is_muted: false,
|
||||
is_web_public: false,
|
||||
message_retention_days: null,
|
||||
name,
|
||||
newly_subscribed: false,
|
||||
pin_to_top: false,
|
||||
previously_subscribed: false,
|
||||
push_notifications: false,
|
||||
rendered_description: `<p>Description of ${name}</p>`,
|
||||
stream_id,
|
||||
/* STREAM_POST_POLICY_EVERYONE */
|
||||
stream_post_policy: 1,
|
||||
stream_weekly_traffic: 0,
|
||||
/* Most tests want to work with a channel the current user is subscribed to. */
|
||||
subscribed: true,
|
||||
topics_policy: "inherit",
|
||||
wildcard_mentions_notify: false,
|
||||
folder_id: null,
|
||||
};
|
||||
|
||||
return {...default_channel, ...opts};
|
||||
};
|
||||
73
web/tests/lib/example_user.cjs
Normal file
73
web/tests/lib/example_user.cjs
Normal file
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
|
||||
let last_issued_user_id = 1000;
|
||||
|
||||
const get_user_id = () => {
|
||||
last_issued_user_id += 1 + Math.floor(Math.random() * 100);
|
||||
return last_issued_user_id;
|
||||
};
|
||||
|
||||
const Role = Object.freeze({
|
||||
OWNER: 100,
|
||||
ADMINISTRATOR: 200,
|
||||
MODERATOR: 300,
|
||||
MEMBER: 400,
|
||||
GUEST: 600,
|
||||
});
|
||||
|
||||
const Bot = Object.freeze({
|
||||
GENERIC: 1,
|
||||
INCOMING_WEBHOOK: 2,
|
||||
OUTGOING_WEBHOOK: 3,
|
||||
EMBEDDED: 4,
|
||||
});
|
||||
|
||||
const bot_or_user_props = (opts = {}) => {
|
||||
// Since other fields need `user_id`, we extract it early.
|
||||
const user_id = opts.user_id ?? get_user_id();
|
||||
const role = opts.role ?? Role.MEMBER;
|
||||
|
||||
const common_props = {
|
||||
user_id,
|
||||
delivery_email: opts.delivery_email ?? null,
|
||||
email: `user-${user_id}@example.org`,
|
||||
full_name: `user-${user_id}-ex_name`,
|
||||
date_joined: Date.now(),
|
||||
// Derived from role, matching people._add_user() production logic.
|
||||
is_owner: role === Role.OWNER,
|
||||
is_admin: role === Role.OWNER || role === Role.ADMINISTRATOR,
|
||||
is_guest: role === Role.GUEST,
|
||||
is_moderator: role === Role.OWNER || role === Role.ADMINISTRATOR || role === Role.MODERATOR,
|
||||
timezone: "UTC",
|
||||
avatar_version: 0,
|
||||
role,
|
||||
};
|
||||
|
||||
return {...common_props, ...opts};
|
||||
};
|
||||
|
||||
const make_user = (opts = {}) => ({
|
||||
...bot_or_user_props(opts),
|
||||
is_bot: opts.is_bot ?? false,
|
||||
// By default an empty dictionary.
|
||||
profile_data: opts.profile_data ?? {},
|
||||
});
|
||||
|
||||
const make_bot = (opts = {}) => ({
|
||||
...bot_or_user_props(opts),
|
||||
is_bot: true,
|
||||
// By default a generic bot.
|
||||
bot_type: opts.bot_type ?? Bot.GENERIC,
|
||||
bot_owner_id: opts.bot_owner_id ?? null,
|
||||
});
|
||||
|
||||
const make_cross_realm_bot = (opts = {}) => ({
|
||||
...make_bot(opts),
|
||||
is_system_bot: true,
|
||||
});
|
||||
|
||||
exports.make_bot = make_bot;
|
||||
exports.make_user = make_user;
|
||||
exports.make_cross_realm_bot = make_cross_realm_bot;
|
||||
exports.Role = Role;
|
||||
exports.Bot = Bot;
|
||||
51
web/tests/lib/handlebars.cjs
Normal file
51
web/tests/lib/handlebars.cjs
Normal file
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const Handlebars = require("handlebars");
|
||||
const {SourceMapConsumer, SourceNode} = require("source-map");
|
||||
|
||||
const hb = Handlebars.create();
|
||||
|
||||
class ZJavaScriptCompiler extends hb.JavaScriptCompiler {
|
||||
nameLookup(parent, name, type) {
|
||||
// Auto-register partials with relative paths, like handlebars-loader.
|
||||
if (type === "partial" && name !== "@partial-block") {
|
||||
const filename = path.resolve(path.dirname(this.options.srcName), name + ".hbs");
|
||||
return ["require(", JSON.stringify(filename), ")"];
|
||||
}
|
||||
return super.nameLookup(parent, name, type);
|
||||
}
|
||||
}
|
||||
|
||||
ZJavaScriptCompiler.prototype.compiler = ZJavaScriptCompiler;
|
||||
hb.JavaScriptCompiler = ZJavaScriptCompiler;
|
||||
|
||||
function compile_hbs(module, filename) {
|
||||
const code = fs.readFileSync(filename, "utf8");
|
||||
const pc = hb.precompile(code, {
|
||||
preventIndent: true,
|
||||
srcName: filename,
|
||||
strict: true,
|
||||
explicitPartialContext: true,
|
||||
});
|
||||
const node = new SourceNode();
|
||||
node.add([
|
||||
'const Handlebars = require("handlebars/runtime.js");\n',
|
||||
"module.exports = Handlebars.template(",
|
||||
SourceNode.fromStringWithSourceMap(pc.code, new SourceMapConsumer(pc.map)),
|
||||
");\n",
|
||||
]);
|
||||
const out = node.toStringWithSourceMap();
|
||||
module._compile(
|
||||
out.code +
|
||||
"\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," +
|
||||
Buffer.from(out.map.toString()).toString("base64"),
|
||||
filename,
|
||||
);
|
||||
}
|
||||
|
||||
exports.hook_require = () => {
|
||||
require.extensions[".hbs"] = compile_hbs;
|
||||
};
|
||||
14
web/tests/lib/i18n.cjs
Normal file
14
web/tests/lib/i18n.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
const {zrequire} = require("./namespace.cjs");
|
||||
|
||||
const i18n = zrequire("i18n");
|
||||
|
||||
Object.assign(exports, i18n);
|
||||
|
||||
exports.$t = (descriptor, values, opts) =>
|
||||
i18n.$t({id: descriptor.defaultMessage, descriptor}, values, opts);
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.$t_html = (descriptor, values, opts) =>
|
||||
i18n.$t_html({id: descriptor.defaultMessage, descriptor}, values, opts);
|
||||
129
web/tests/lib/index.cjs
Normal file
129
web/tests/lib/index.cjs
Normal file
@@ -0,0 +1,129 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const path = require("node:path");
|
||||
|
||||
require("@date-fns/tz"); // To prevent @sinonjs/fake-timers from interfering with it
|
||||
require("css.escape");
|
||||
require("handlebars/runtime.js");
|
||||
const {JSDOM} = require("jsdom");
|
||||
|
||||
const handlebars = require("./handlebars.cjs");
|
||||
const namespace = require("./namespace.cjs");
|
||||
const test = require("./test.cjs");
|
||||
const blueslip = require("./zblueslip.cjs");
|
||||
const {$} = require("./zjquery.cjs");
|
||||
const zpage_billing_params = require("./zpage_billing_params.cjs");
|
||||
const zpage_params = require("./zpage_params.cjs");
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
|
||||
const dom = new JSDOM("", {url: "http://zulip.zulipdev.com/"});
|
||||
global.DOMParser = dom.window.DOMParser;
|
||||
global.HTMLAnchorElement = dom.window.HTMLAnchorElement;
|
||||
global.HTMLElement = dom.window.HTMLElement;
|
||||
global.HTMLImageElement = dom.window.HTMLImageElement;
|
||||
global.Window = dom.window.Window;
|
||||
Object.defineProperty(global, "navigator", {
|
||||
value: {
|
||||
userAgent: "node.js",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
require("@babel/register").default({
|
||||
extensions: [".cjs", ".cts", ".js", ".mjs", ".mts", ".ts"],
|
||||
root: path.resolve(__dirname, "../.."),
|
||||
});
|
||||
|
||||
// Find the files we need to run.
|
||||
const files = process.argv.slice(2);
|
||||
assert.notEqual(files.length, 0, "No tests found");
|
||||
|
||||
// Set up our namespace helpers.
|
||||
const window = new Proxy(global, {
|
||||
set(_obj, prop, value) {
|
||||
namespace.set_global(prop, value);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const ls_container = new Map();
|
||||
const localStorage = {
|
||||
getItem(key) {
|
||||
return ls_container.get(key);
|
||||
},
|
||||
setItem(key, val) {
|
||||
ls_container.set(key, val);
|
||||
},
|
||||
/* istanbul ignore next */
|
||||
removeItem(key) {
|
||||
ls_container.delete(key);
|
||||
},
|
||||
clear() {
|
||||
ls_container.clear();
|
||||
},
|
||||
};
|
||||
|
||||
// Set up Handlebars
|
||||
handlebars.hook_require();
|
||||
|
||||
require("../../src/templates.ts"); // register Zulip extensions
|
||||
|
||||
async function run_one_module(file) {
|
||||
$.clear_all_elements();
|
||||
console.info("running test " + path.basename(file, ".test.cjs"));
|
||||
test.set_current_file_name(file);
|
||||
test.suite.length = 0;
|
||||
require(file);
|
||||
for (const f of test.suite) {
|
||||
await f();
|
||||
}
|
||||
namespace.complain_about_unused_mocks();
|
||||
}
|
||||
|
||||
test.set_verbose(files.length === 1);
|
||||
|
||||
// In case someone mistakenly vanishes the async task with something like `await
|
||||
// new Promise(() => {})`, assume failure until we establish otherwise.
|
||||
process.exitCode = 1;
|
||||
|
||||
(async () => {
|
||||
let exit_code = 0;
|
||||
|
||||
for (const file of files) {
|
||||
namespace.start();
|
||||
namespace.set_global("window", window);
|
||||
namespace.set_global("location", dom.window.location);
|
||||
window.location.href = "http://zulip.zulipdev.com/#";
|
||||
namespace.set_global("localStorage", localStorage);
|
||||
ls_container.clear();
|
||||
zpage_billing_params.reset();
|
||||
zpage_params.reset();
|
||||
|
||||
namespace.mock_esm("../../src/blueslip", blueslip);
|
||||
require("../../src/blueslip.ts");
|
||||
namespace.mock_esm("../../src/base_page_params", zpage_params);
|
||||
require("../../src/base_page_params.ts");
|
||||
namespace.mock_esm("../../src/billing/page_params", zpage_billing_params);
|
||||
require("../../src/billing/page_params.ts");
|
||||
namespace.mock_esm("../../src/page_params", zpage_params);
|
||||
require("../../src/page_params.ts");
|
||||
|
||||
// Make sure we re-register our Handlebars helpers.
|
||||
require("../../src/templates.ts");
|
||||
|
||||
try {
|
||||
await run_one_module(file);
|
||||
blueslip.reset();
|
||||
} catch (error) /* istanbul ignore next */ {
|
||||
console.error(error);
|
||||
exit_code = 1;
|
||||
blueslip.reset(true);
|
||||
}
|
||||
|
||||
namespace.finish();
|
||||
}
|
||||
|
||||
process.exitCode = exit_code;
|
||||
})();
|
||||
203
web/tests/lib/markdown_assert.cjs
Normal file
203
web/tests/lib/markdown_assert.cjs
Normal file
@@ -0,0 +1,203 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* markdown_assert.js
|
||||
*
|
||||
* Used to determine whether two Markdown HTML strings are semantically
|
||||
* equivalent. Differs from the naive string-comparison approach in that
|
||||
* differently typed but equivalent HTML fragments, such as '<p>"</p>'
|
||||
* and '<p>\"</p>', and '<span attr1="a" attr2="b"></span>' and
|
||||
* '<span attr2="a" attr1="b"></span>', are still considered equal.
|
||||
*
|
||||
* The exported method equal() serves as a drop-in replacement for
|
||||
* assert.equal(). Likewise, the exported method notEqual() replaces
|
||||
* assert.notEqual().
|
||||
*
|
||||
* There is a default _output_formatter used to create the
|
||||
* AssertionError error message; this function can be overridden using
|
||||
* the exported setFormatter() function below.
|
||||
*
|
||||
* The HTML passed to the _output_formatter is not the original HTML, but
|
||||
* rather a serialized version of a DOM element generated from the original
|
||||
* HTML. This makes it easier to spot relevant differences.
|
||||
*/
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {JSDOM} = require("jsdom");
|
||||
const _ = require("lodash");
|
||||
|
||||
const mdiff = require("./mdiff.cjs");
|
||||
|
||||
// Module-level global instance of MarkdownComparer, initialized when needed
|
||||
let _markdownComparerInstance = null;
|
||||
|
||||
class MarkdownComparer {
|
||||
constructor(output_formatter) {
|
||||
this._output_formatter =
|
||||
output_formatter ||
|
||||
/* istanbul ignore next */
|
||||
function (actual, expected) {
|
||||
return ["Actual and expected output do not match.", actual, "!=", expected].join(
|
||||
"\n",
|
||||
);
|
||||
};
|
||||
this._document = new JSDOM().window.document;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
setFormatter(output_formatter) {
|
||||
this._output_formatter = output_formatter || this._output_formatter;
|
||||
}
|
||||
|
||||
_htmlToElement(html, id) {
|
||||
const template = this._document.createElement("template");
|
||||
const id_node = this._document.createAttribute("id");
|
||||
id_node.value = id;
|
||||
template.setAttributeNode(id_node);
|
||||
template.innerHTML = html;
|
||||
return template;
|
||||
}
|
||||
|
||||
_haveEqualContents(node1, node2) {
|
||||
if (node1.content.childNodes.length !== node2.content.childNodes.length) {
|
||||
return false;
|
||||
}
|
||||
return _.zip(node1.content.childNodes, node2.content.childNodes).every(([child1, child2]) =>
|
||||
child1.isEqualNode(child2),
|
||||
);
|
||||
}
|
||||
|
||||
_reorderAttributes(node) {
|
||||
// Sorts every attribute in every element by name. Ensures consistent diff HTML output
|
||||
|
||||
const attributeList = [...node.attributes];
|
||||
|
||||
// If put in above forEach loop, causes issues (possible nodes.attribute invalidation?)
|
||||
for (const attr of attributeList) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
|
||||
attributeList.sort((a, b) => {
|
||||
const name_a = a.name;
|
||||
const name_b = b.name;
|
||||
if (name_a < name_b) {
|
||||
return -1;
|
||||
}
|
||||
if (name_a > name_b) {
|
||||
return 1;
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Put them back in, in order
|
||||
for (const attribute of attributeList) {
|
||||
node.setAttribute(attribute.name, attribute.value);
|
||||
}
|
||||
|
||||
if (node.hasChildNodes()) {
|
||||
for (const childNode of node.children) {
|
||||
this._reorderAttributes(childNode);
|
||||
}
|
||||
}
|
||||
if (node.content && node.content.hasChildNodes()) {
|
||||
for (const childNode of node.content.children) {
|
||||
this._reorderAttributes(childNode);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
_compare(actual_markdown, expected_markdown) {
|
||||
const ID_ACTUAL = "0";
|
||||
const ID_EXPECTED = "1";
|
||||
|
||||
const element_actual = this._htmlToElement(actual_markdown, ID_ACTUAL);
|
||||
const element_expected = this._htmlToElement(expected_markdown, ID_EXPECTED);
|
||||
|
||||
let html = {};
|
||||
|
||||
const are_equivalent = this._haveEqualContents(element_actual, element_expected);
|
||||
if (!are_equivalent) {
|
||||
html = {
|
||||
actual: this._reorderAttributes(element_actual).innerHTML,
|
||||
expected: this._reorderAttributes(element_expected).innerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
element_actual.remove();
|
||||
element_expected.remove();
|
||||
|
||||
return {are_equivalent, html};
|
||||
}
|
||||
|
||||
assertEqual(actual, expected, message) {
|
||||
const comparison_results = this._compare(actual, expected);
|
||||
|
||||
message ||= "";
|
||||
message += "\n";
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (comparison_results.are_equivalent === false) {
|
||||
throw new assert.AssertionError({
|
||||
message:
|
||||
message +
|
||||
this._output_formatter(
|
||||
comparison_results.html.actual,
|
||||
comparison_results.html.expected,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
assertNotEqual(actual, expected, message) {
|
||||
const comparison_results = this._compare(actual, expected);
|
||||
|
||||
message ||= "";
|
||||
message += "\n";
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (comparison_results.are_equivalent) {
|
||||
throw new assert.AssertionError({
|
||||
message:
|
||||
message +
|
||||
[
|
||||
"actual and expected output produce semantically identical HTML",
|
||||
actual,
|
||||
"==",
|
||||
expected,
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function returnComparer() {
|
||||
if (!_markdownComparerInstance) {
|
||||
_markdownComparerInstance = new MarkdownComparer(
|
||||
/* istanbul ignore next */
|
||||
(actual, expected) =>
|
||||
[
|
||||
"Actual and expected output do not match. Showing diff",
|
||||
mdiff.diff_strings(actual, expected),
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
return _markdownComparerInstance;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
equal(expected, actual, message) {
|
||||
returnComparer().assertEqual(actual, expected, message);
|
||||
},
|
||||
|
||||
notEqual(expected, actual, message) {
|
||||
returnComparer().assertNotEqual(actual, expected, message);
|
||||
},
|
||||
|
||||
/* istanbul ignore next */
|
||||
setFormatter(output_formatter) {
|
||||
returnComparer().setFormatter(output_formatter);
|
||||
},
|
||||
};
|
||||
149
web/tests/lib/mdiff.cjs
Normal file
149
web/tests/lib/mdiff.cjs
Normal file
@@ -0,0 +1,149 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* mdiff.js
|
||||
*
|
||||
* Used to produce colorful and informative diffs for comparison of generated
|
||||
* Markdown. Unlike the built-in diffs used in python or node.js assert libraries,
|
||||
* is actually designed to be effective for long, single-line comparisons.
|
||||
*
|
||||
* Based on diffing library difflib, a js port of the python library.
|
||||
*
|
||||
* The sole exported function diff_strings(string_0, string_1) returns a pretty-printed
|
||||
* Unicode string containing their diff.
|
||||
*/
|
||||
|
||||
const difflib = require("difflib");
|
||||
|
||||
function apply_color(input_string, changes) {
|
||||
let previous_index = 0;
|
||||
let processed_string = input_string.slice(0, 2);
|
||||
input_string = input_string.slice(2);
|
||||
|
||||
const formatter = new Map([
|
||||
["delete", (string) => "\u{1B}[31m" + string + "\u{1B}[0m"],
|
||||
["insert", (string) => "\u{1B}[32m" + string + "\u{1B}[0m"],
|
||||
["replace", (string) => "\u{1B}[33m" + string + "\u{1B}[0m"],
|
||||
]);
|
||||
for (const change of changes) {
|
||||
if (formatter.has(change.tag)) {
|
||||
processed_string += input_string.slice(previous_index, change.beginning_index);
|
||||
processed_string += formatter.get(change.tag)(
|
||||
input_string.slice(change.beginning_index, change.ending_index),
|
||||
);
|
||||
previous_index = change.ending_index;
|
||||
}
|
||||
}
|
||||
|
||||
processed_string += input_string.slice(previous_index);
|
||||
return processed_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The library difflib produces diffs that look as follows:
|
||||
*
|
||||
* - <p>upgrade! yes</p>
|
||||
* ? ^^ -
|
||||
* + <p>downgrade yes.</p>
|
||||
* ? ^^^^ +
|
||||
*
|
||||
* The purpose of this function is to facilitate converting these diffs into
|
||||
* colored versions, where the question-mark lines are removed, replaced with
|
||||
* directions to add appropriate color to the lines that they annotate.
|
||||
*/
|
||||
function parse_questionmark_line(questionmark_line) {
|
||||
let current_sequence = ""; // Either "^", "-", "+", or ""
|
||||
let beginning_index = 0;
|
||||
let index = 0;
|
||||
|
||||
const changes_list = [];
|
||||
const aliases = new Map([
|
||||
["^", "replace"],
|
||||
["+", "insert"],
|
||||
["-", "delete"],
|
||||
]);
|
||||
const add_change = () => {
|
||||
if (current_sequence) {
|
||||
changes_list.push({
|
||||
tag: aliases.get(current_sequence),
|
||||
beginning_index,
|
||||
ending_index: index,
|
||||
});
|
||||
current_sequence = "";
|
||||
}
|
||||
};
|
||||
|
||||
questionmark_line = questionmark_line.slice(2).trimRight("\n");
|
||||
|
||||
for (const character of questionmark_line) {
|
||||
if (aliases.has(character)) {
|
||||
if (current_sequence !== character) {
|
||||
add_change();
|
||||
current_sequence = character;
|
||||
beginning_index = index;
|
||||
}
|
||||
} else {
|
||||
add_change();
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// In case we have a "change" involving the last character on a line
|
||||
// e.g. a string such as "? ^^ -- ++++"
|
||||
add_change();
|
||||
|
||||
return changes_list;
|
||||
}
|
||||
|
||||
function diff_strings(string_0, string_1) {
|
||||
let output_lines = [];
|
||||
|
||||
const ndiff_output = difflib.ndiff(string_0.split("\n"), string_1.split("\n"));
|
||||
|
||||
for (const line of ndiff_output) {
|
||||
if (line.startsWith("+")) {
|
||||
output_lines.push(line);
|
||||
} else if (line.startsWith("-")) {
|
||||
output_lines.push(line);
|
||||
} else if (line.startsWith("?")) {
|
||||
const changes_list = parse_questionmark_line(line);
|
||||
output_lines.push(apply_color(output_lines.pop(), changes_list));
|
||||
} else {
|
||||
output_lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
output_lines = output_lines.map(
|
||||
(string) => "\u{1B}[34m" + string.slice(0, 1) + "\u{1B}[0m" + string.slice(1),
|
||||
);
|
||||
|
||||
return output_lines.join("\n");
|
||||
}
|
||||
|
||||
module.exports = {diff_strings};
|
||||
|
||||
// Simple CLI for this module
|
||||
// Only run this code if called as a command-line utility
|
||||
if (require.main === module) {
|
||||
const {parseArgs} = require("node:util");
|
||||
|
||||
const usage =
|
||||
"Usage: mdiff <string_0> <string_1>\nWhere <string_0> and <string_1> are the strings to be diffed";
|
||||
const {
|
||||
values: {help},
|
||||
positionals,
|
||||
} = parseArgs({options: {help: {type: "boolean"}}, allowPositionals: true});
|
||||
const [string_0, string_1] = positionals;
|
||||
|
||||
if (help) {
|
||||
console.log(usage);
|
||||
process.exit(0);
|
||||
}
|
||||
if (string_1 === undefined) {
|
||||
console.error(usage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const output = diff_strings(string_0, string_1);
|
||||
console.log(output);
|
||||
}
|
||||
19
web/tests/lib/message_list.cjs
Normal file
19
web/tests/lib/message_list.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const {zrequire} = require("./namespace.cjs");
|
||||
|
||||
const {Filter} = zrequire("filter");
|
||||
const {MessageList} = zrequire("message_list");
|
||||
const {MessageListData} = zrequire("message_list_data");
|
||||
|
||||
exports.make_message_list = (filter_terms, opts = {}) => {
|
||||
const filter = new Filter(filter_terms);
|
||||
const default_message_list = new MessageList({
|
||||
data: new MessageListData({
|
||||
filter,
|
||||
}),
|
||||
is_node_test: true,
|
||||
});
|
||||
default_message_list.data.participants.humans = new Set(opts.visible_participants);
|
||||
return default_message_list;
|
||||
};
|
||||
11
web/tests/lib/mock_channel.cjs
Normal file
11
web/tests/lib/mock_channel.cjs
Normal file
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
// We have to pass channel here so that the test file mocks channel.ts before
|
||||
// tests start running, and so that channel.ts is only mocked once.
|
||||
exports.mock_channel_get = function (channel, f) {
|
||||
channel.get = async (opts) => {
|
||||
// await an empty promise to ensure the "fetch" happens asynchronously
|
||||
await Promise.resolve();
|
||||
f(opts);
|
||||
};
|
||||
};
|
||||
434
web/tests/lib/namespace.cjs
Normal file
434
web/tests/lib/namespace.cjs
Normal file
@@ -0,0 +1,434 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const Module = require("node:module");
|
||||
const path = require("node:path");
|
||||
|
||||
const FakeTimers = require("@sinonjs/fake-timers");
|
||||
const {default: callsites} = require("callsites");
|
||||
|
||||
const {$} = require("./zjquery.cjs");
|
||||
|
||||
const new_globals = new Set();
|
||||
let old_globals = {};
|
||||
|
||||
let actual_load;
|
||||
const module_mocks = new Map();
|
||||
const template_mocks = new Map();
|
||||
const used_module_mocks = new Set();
|
||||
const used_templates = new Set();
|
||||
|
||||
const jquery_path = require.resolve("jquery");
|
||||
|
||||
let in_mid_render = false;
|
||||
let jquery_function;
|
||||
|
||||
const template_path = path.resolve(__dirname, "../../templates");
|
||||
|
||||
function load(request, parent, isMain) {
|
||||
const filename = Module._resolveFilename(request, parent, isMain);
|
||||
if (module_mocks.has(filename)) {
|
||||
used_module_mocks.add(filename);
|
||||
return module_mocks.get(filename);
|
||||
}
|
||||
if (filename.endsWith(".hbs") && filename.startsWith(template_path + path.sep)) {
|
||||
const actual_render = actual_load(request, parent, isMain);
|
||||
return template_stub({filename, actual_render});
|
||||
}
|
||||
if (filename === jquery_path) {
|
||||
return {$: jquery_function ?? $};
|
||||
}
|
||||
|
||||
const module = actual_load(request, parent, isMain);
|
||||
if ((typeof module === "object" || typeof module === "function") && "__esModule" in module) {
|
||||
/* istanbul ignore next */
|
||||
function error_immutable() {
|
||||
throw new Error(`${filename} is an immutable ES module`);
|
||||
}
|
||||
return new Proxy(module, {
|
||||
defineProperty: error_immutable,
|
||||
deleteProperty: error_immutable,
|
||||
preventExtensions: error_immutable,
|
||||
set: error_immutable,
|
||||
setPrototypeOf: error_immutable,
|
||||
});
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
function template_stub({filename, actual_render}) {
|
||||
return function render(...args) {
|
||||
// If our template was not mocked or is being rendered as a
|
||||
// partial, use the actual implementation.
|
||||
if (!template_mocks.has(filename) || in_mid_render) {
|
||||
return actual_render(...args);
|
||||
}
|
||||
|
||||
used_templates.add(filename);
|
||||
|
||||
const {exercise_template, f} = template_mocks.get(filename);
|
||||
|
||||
const data = args[0];
|
||||
|
||||
let html;
|
||||
if (exercise_template) {
|
||||
// If our dev wants to exercise the actual template, then do so.
|
||||
// We set the in_mid_render bool so that included (i.e. partial)
|
||||
// templates get rendered.
|
||||
in_mid_render = true;
|
||||
html = actual_render(...args);
|
||||
in_mid_render = false;
|
||||
}
|
||||
|
||||
const mock_html = f(data, html);
|
||||
assert.equal(
|
||||
typeof mock_html,
|
||||
"string",
|
||||
`The template mock for ${filename} must return a string`,
|
||||
);
|
||||
return mock_html;
|
||||
};
|
||||
}
|
||||
|
||||
exports.clock = FakeTimers.install();
|
||||
|
||||
exports.start = () => {
|
||||
assert.equal(actual_load, undefined, "namespace.start was called twice in a row.");
|
||||
actual_load = Module._load;
|
||||
Module._load = load;
|
||||
};
|
||||
|
||||
// We provide `mock_cjs` for mocking a CommonJS module, and `mock_esm` for
|
||||
// mocking an ES6 module.
|
||||
//
|
||||
// A CommonJS module:
|
||||
// - loads other modules using `require()`,
|
||||
// - assigns its public contents to the `exports` object or `module.exports`,
|
||||
// - consists of a single JavaScript value, typically an object or function,
|
||||
// - when imported by an ES6 module:
|
||||
// * is shallow-copied to a collection of immutable bindings, if it's an
|
||||
// object,
|
||||
// * is converted to a single default binding, if not.
|
||||
//
|
||||
// An ES6 module:
|
||||
// - loads other modules using `import`,
|
||||
// - declares its public contents using `export` statements,
|
||||
// - consists of a collection of live bindings that may be mutated from inside
|
||||
// but not outside the module,
|
||||
// - may have a default binding (that's just syntactic sugar for a binding
|
||||
// named `default`),
|
||||
// - when required by a CommonJS module, always appears as an object.
|
||||
//
|
||||
// Most of our own modules are ES6 modules.
|
||||
//
|
||||
// For a third party module available in both formats that might present two
|
||||
// incompatible APIs (especially if the CommonJS module is a function),
|
||||
// Webpack will prefer the ES6 module if its availability is indicated by the
|
||||
// "module" field of package.json, while Node.js will not; we need to mock the
|
||||
// format preferred by Webpack.
|
||||
|
||||
exports.mock_cjs = (module_path, obj, {callsite = callsites()[1]} = {}) => {
|
||||
assert.notEqual(
|
||||
module_path,
|
||||
"jquery",
|
||||
"We automatically mock jquery to zjquery. Grep for mock_jquery if you want more control.",
|
||||
);
|
||||
|
||||
const filename = Module._resolveFilename(
|
||||
module_path,
|
||||
require.cache[callsite.getFileName()],
|
||||
false,
|
||||
);
|
||||
|
||||
assert.ok(!module_mocks.has(filename), `You already set up a mock for ${filename}`);
|
||||
|
||||
assert.ok(
|
||||
!Object.hasOwn(require.cache, filename),
|
||||
`It is too late to mock ${filename}; call this earlier.`,
|
||||
);
|
||||
|
||||
module_mocks.set(filename, obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
exports.mock_jquery = ($) => {
|
||||
jquery_function = $; // eslint-disable-line no-jquery/variable-pattern
|
||||
return $;
|
||||
};
|
||||
|
||||
exports._start_template_mocking = () => {
|
||||
template_mocks.clear();
|
||||
used_templates.clear();
|
||||
};
|
||||
|
||||
exports._finish_template_mocking = () => {
|
||||
for (const filename of template_mocks.keys()) {
|
||||
assert.ok(
|
||||
used_templates.has(filename),
|
||||
`You called mock_template with ${filename} but we never saw it get used.`,
|
||||
);
|
||||
}
|
||||
template_mocks.clear();
|
||||
used_templates.clear();
|
||||
};
|
||||
|
||||
exports._mock_template = (fn, exercise_template, f) => {
|
||||
template_mocks.set(path.join(template_path, fn), {exercise_template, f});
|
||||
};
|
||||
|
||||
exports.mock_esm = (module_path, obj = {}, {callsite = callsites()[1]} = {}) => {
|
||||
assert.equal(typeof obj, "object", "An ES module must be mocked with an object");
|
||||
return exports.mock_cjs(module_path, {...obj, __esModule: true}, {callsite});
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.unmock_module = (module_path, {callsite = callsites()[1]} = {}) => {
|
||||
const filename = Module._resolveFilename(
|
||||
module_path,
|
||||
require.cache[callsite.getFileName()],
|
||||
false,
|
||||
);
|
||||
|
||||
assert.ok(module_mocks.has(filename), `Cannot unmock ${filename}, which was not mocked`);
|
||||
|
||||
assert.ok(
|
||||
used_module_mocks.has(filename),
|
||||
`You asked to mock ${filename} but we never saw it during compilation.`,
|
||||
);
|
||||
|
||||
module_mocks.delete(filename);
|
||||
used_module_mocks.delete(filename);
|
||||
};
|
||||
|
||||
exports.set_global = function (name, val) {
|
||||
assert.notEqual(val, null, `We try to avoid using null in our codebase.`);
|
||||
|
||||
if (!Object.hasOwn(old_globals, name)) {
|
||||
if (!Object.hasOwn(global, name)) {
|
||||
new_globals.add(name);
|
||||
}
|
||||
old_globals[name] = global[name];
|
||||
}
|
||||
global[name] = val;
|
||||
return val;
|
||||
};
|
||||
|
||||
exports.zrequire = function (short_fn) {
|
||||
assert.notEqual(
|
||||
short_fn,
|
||||
"templates",
|
||||
`
|
||||
There is no need to zrequire templates.ts.
|
||||
|
||||
The test runner automatically registers the
|
||||
Handlebars extensions.
|
||||
`,
|
||||
);
|
||||
|
||||
return require(`../../src/${short_fn}`);
|
||||
};
|
||||
|
||||
const webPath = path.resolve(__dirname, "../..") + path.sep;
|
||||
const testsLibPath = __dirname + path.sep;
|
||||
|
||||
exports.complain_about_unused_mocks = function () {
|
||||
for (const filename of module_mocks.keys()) {
|
||||
/* istanbul ignore if */
|
||||
if (!used_module_mocks.has(filename)) {
|
||||
console.error(`You asked to mock ${filename} but we never saw it during compilation.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.finish = function () {
|
||||
/*
|
||||
Handle cleanup tasks after we've run one module.
|
||||
|
||||
Note that we currently do lazy compilation of modules,
|
||||
so we need to wait till the module tests finish
|
||||
running to do things like detecting pointless mocks
|
||||
and resetting our _load hook.
|
||||
*/
|
||||
exports.clock.reset();
|
||||
jquery_function = undefined;
|
||||
|
||||
assert.notEqual(actual_load, undefined, "namespace.finish was called without namespace.start.");
|
||||
Module._load = actual_load;
|
||||
actual_load = undefined;
|
||||
|
||||
module_mocks.clear();
|
||||
used_module_mocks.clear();
|
||||
|
||||
for (const path of Object.keys(require.cache)) {
|
||||
if (path.startsWith(webPath) && !path.startsWith(testsLibPath)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete require.cache[path];
|
||||
}
|
||||
}
|
||||
Object.assign(global, old_globals);
|
||||
old_globals = {};
|
||||
for (const name of new_globals) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete global[name];
|
||||
}
|
||||
new_globals.clear();
|
||||
};
|
||||
|
||||
exports.with_overrides = function (test_function) {
|
||||
// This function calls test_function() and passes in
|
||||
// a way to override the namespace temporarily.
|
||||
|
||||
const restore_callbacks = [];
|
||||
let ok = false;
|
||||
|
||||
const override = function (obj, prop, value, {unused = true} = {}) {
|
||||
// Given an object `obj` (which is usually a module object),
|
||||
// we re-map `obj[prop]` to the `value` passed in by the caller.
|
||||
// Then the outer function here (`with_overrides`) automatically
|
||||
// restores the original value of `obj[prop]` as its last
|
||||
// step. Generally our code calls `run_test`, which wraps
|
||||
// `with_overrides`.
|
||||
|
||||
assert.ok(
|
||||
typeof obj === "object" || typeof obj === "function",
|
||||
`We cannot override a function for ${typeof obj} objects`,
|
||||
);
|
||||
|
||||
const had_value = Object.hasOwn(obj, prop);
|
||||
const old_value = obj[prop];
|
||||
let new_value = value;
|
||||
|
||||
if (typeof value === "function") {
|
||||
assert.ok(
|
||||
old_value === undefined || typeof old_value === "function",
|
||||
`
|
||||
You are overriding a non-function with a function.
|
||||
This is almost certainly an error.
|
||||
`,
|
||||
);
|
||||
|
||||
new_value = function (...args) {
|
||||
unused = false;
|
||||
return value.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_value._patched_with_override = true;
|
||||
} else {
|
||||
unused = false;
|
||||
}
|
||||
|
||||
obj[prop] = new_value;
|
||||
restore_callbacks.push(() => {
|
||||
if (ok) {
|
||||
assert.ok(!unused, `${prop} never got invoked!`);
|
||||
}
|
||||
if (had_value) {
|
||||
obj[prop] = old_value;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete obj[prop];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const disallow = function (obj, prop) {
|
||||
override(
|
||||
obj,
|
||||
prop,
|
||||
// istanbul ignore next
|
||||
() => {
|
||||
throw new Error(`unexpected call to ${prop}`);
|
||||
},
|
||||
{unused: false},
|
||||
);
|
||||
};
|
||||
|
||||
const override_rewire = function (obj, prop, value, {unused = true} = {}) {
|
||||
assert.ok(
|
||||
typeof obj === "object" || typeof obj === "function",
|
||||
`We cannot override a function for ${typeof obj} objects`,
|
||||
);
|
||||
|
||||
assert.ok(Object.hasOwn(obj, prop));
|
||||
const old_value = obj[prop];
|
||||
let new_value = value;
|
||||
|
||||
if (typeof value === "function") {
|
||||
assert.ok(
|
||||
typeof old_value === "function",
|
||||
`
|
||||
You are overriding a non-function with a function.
|
||||
This is almost certainly an error.
|
||||
`,
|
||||
);
|
||||
|
||||
new_value = function (...args) {
|
||||
unused = false;
|
||||
return value.apply(this, args);
|
||||
};
|
||||
} else {
|
||||
unused = false;
|
||||
}
|
||||
|
||||
const rewire_prop = `rewire_${prop}`;
|
||||
/* istanbul ignore if */
|
||||
if (!Object.hasOwn(obj, rewire_prop)) {
|
||||
assert.fail(`You must define ${rewire_prop} to use override_rewire on ${prop}.`);
|
||||
}
|
||||
obj[rewire_prop](new_value);
|
||||
restore_callbacks.push(() => {
|
||||
if (ok) {
|
||||
assert.ok(!unused, `${prop} never got invoked!`);
|
||||
}
|
||||
obj[rewire_prop](old_value);
|
||||
});
|
||||
};
|
||||
|
||||
const disallow_rewire = function (obj, prop) {
|
||||
override_rewire(
|
||||
obj,
|
||||
prop,
|
||||
// istanbul ignore next
|
||||
() => {
|
||||
throw new Error(`unexpected call to ${prop}`);
|
||||
},
|
||||
{unused: false},
|
||||
);
|
||||
};
|
||||
|
||||
let ret;
|
||||
let is_promise = false;
|
||||
try {
|
||||
ret = test_function({override, override_rewire, disallow, disallow_rewire});
|
||||
is_promise = typeof ret?.then === "function";
|
||||
ok = !is_promise;
|
||||
} finally {
|
||||
if (!is_promise) {
|
||||
restore_callbacks.reverse();
|
||||
for (const restore_callback of restore_callbacks) {
|
||||
restore_callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_promise) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return (async () => {
|
||||
try {
|
||||
ret = await ret;
|
||||
ok = true;
|
||||
return ret;
|
||||
} finally {
|
||||
restore_callbacks.reverse();
|
||||
for (const restore_callback of restore_callbacks) {
|
||||
restore_callback();
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
39
web/tests/lib/stub.cjs
Normal file
39
web/tests/lib/stub.cjs
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
// Stubs don't do any magical modifications to your namespace. They
|
||||
// just provide you a function that records what arguments get passed
|
||||
// to it. To use stubs as something more like "spies," use something
|
||||
// like set_global() to override your namespace.
|
||||
|
||||
exports.make_stub = function () {
|
||||
const self = {num_calls: 0};
|
||||
|
||||
self.f = function (...args) {
|
||||
self.last_call_args = args;
|
||||
self.num_calls += 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
self.get_args = function (...param_names) {
|
||||
const result = {};
|
||||
|
||||
for (const [i, name] of param_names.entries()) {
|
||||
result[name] = self.last_call_args[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
(function test_ourselves() {
|
||||
const stub = exports.make_stub();
|
||||
stub.f("blue", 42);
|
||||
assert.equal(stub.num_calls, 1);
|
||||
const args = stub.get_args("color", "n");
|
||||
assert.equal(args.color, "blue");
|
||||
assert.equal(args.n, 42);
|
||||
})();
|
||||
55
web/tests/lib/test.cjs
Normal file
55
web/tests/lib/test.cjs
Normal file
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
const namespace = require("./namespace.cjs");
|
||||
const zblueslip = require("./zblueslip.cjs");
|
||||
const {$} = require("./zjquery.cjs");
|
||||
const zpage_billing_params = require("./zpage_billing_params.cjs");
|
||||
const zpage_params = require("./zpage_params.cjs");
|
||||
|
||||
let current_file_name;
|
||||
let verbose = false;
|
||||
|
||||
exports.set_current_file_name = (value) => {
|
||||
current_file_name = value;
|
||||
};
|
||||
|
||||
exports.set_verbose = (value) => {
|
||||
verbose = value;
|
||||
};
|
||||
|
||||
exports.noop = () => {};
|
||||
|
||||
exports.suite = [];
|
||||
|
||||
async function execute_test(label, f) {
|
||||
/* istanbul ignore if */
|
||||
if (verbose) {
|
||||
console.info(" test: " + label);
|
||||
}
|
||||
|
||||
$.clear_all_elements();
|
||||
zpage_billing_params.reset();
|
||||
zpage_params.reset();
|
||||
|
||||
try {
|
||||
namespace._start_template_mocking();
|
||||
await namespace.with_overrides(async (helpers) => {
|
||||
await f({
|
||||
...helpers,
|
||||
mock_template: namespace._mock_template,
|
||||
});
|
||||
});
|
||||
namespace._finish_template_mocking();
|
||||
} catch (error) /* istanbul ignore next */ {
|
||||
console.info("-".repeat(50));
|
||||
console.info(`test failed: ${current_file_name} > ${label}`);
|
||||
console.info();
|
||||
throw error;
|
||||
}
|
||||
// defensively reset blueslip after each test.
|
||||
zblueslip.reset();
|
||||
}
|
||||
|
||||
exports.run_test = (label, f) => {
|
||||
exports.suite.push(() => execute_test(label, f));
|
||||
};
|
||||
123
web/tests/lib/zblueslip.cjs
Normal file
123
web/tests/lib/zblueslip.cjs
Normal file
@@ -0,0 +1,123 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
function make_zblueslip() {
|
||||
const lib = {};
|
||||
|
||||
const opts = {
|
||||
// Silently swallow all debug, log and info calls.
|
||||
debug: false,
|
||||
log: false,
|
||||
info: false,
|
||||
// Check against expected error values for the following.
|
||||
warn: true,
|
||||
error: true,
|
||||
};
|
||||
const names = Object.keys(opts);
|
||||
|
||||
// For fatal messages, we should use assert.throws
|
||||
/* istanbul ignore next */
|
||||
lib.fatal = (msg) => {
|
||||
throw new Error(msg);
|
||||
};
|
||||
|
||||
// Store valid test data for options.
|
||||
lib.test_data = {};
|
||||
lib.test_logs = {};
|
||||
|
||||
for (const name of names) {
|
||||
lib.test_data[name] = [];
|
||||
lib.test_logs[name] = [];
|
||||
}
|
||||
|
||||
lib.expect = (name, message, expected_count = 1) => {
|
||||
assert.notEqual(opts[name], undefined, `unexpected arg for expect: ${name}`);
|
||||
assert.ok(
|
||||
expected_count > 0 && Number.isSafeInteger(expected_count),
|
||||
"expected count should be a positive integer",
|
||||
);
|
||||
const obj = {message, count: 0, expected_count};
|
||||
lib.test_data[name].push(obj);
|
||||
};
|
||||
|
||||
const check_seen_messages = () => {
|
||||
for (const name of names) {
|
||||
const logs = lib.test_logs[name];
|
||||
const data = lib.test_data[name];
|
||||
for (const obj of logs) {
|
||||
const message = obj.message;
|
||||
const i = data.findIndex((x) => x.message === message);
|
||||
if (i === -1) {
|
||||
// Only throw this for message types we want to explicitly track.
|
||||
// For example, we do not want to throw here for debug messages.
|
||||
if (opts[name] ?? false) {
|
||||
throw new Error(`Unexpected '${name}' message: ${message}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
data[i].count += 1;
|
||||
}
|
||||
|
||||
for (const obj of data) {
|
||||
const message = obj.message;
|
||||
assert.equal(
|
||||
obj.count,
|
||||
obj.expected_count,
|
||||
`Expected ${obj.expected_count} of '${name}': ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
lib.reset = (skip_checks = false) => {
|
||||
if (!skip_checks) {
|
||||
check_seen_messages();
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
lib.test_data[name] = [];
|
||||
lib.test_logs[name] = [];
|
||||
}
|
||||
};
|
||||
|
||||
lib.get_test_logs = (name) => lib.test_logs[name];
|
||||
|
||||
// Create logging functions
|
||||
for (const name of names) {
|
||||
if (!(opts[name] ?? false)) {
|
||||
// should just log the message.
|
||||
lib[name] = function (message, more_info, cause) {
|
||||
lib.test_logs[name].push({message, more_info, cause});
|
||||
};
|
||||
continue;
|
||||
}
|
||||
lib[name] = function (message, more_info, cause) {
|
||||
/* istanbul ignore if */
|
||||
if (typeof message !== "string") {
|
||||
// We may catch exceptions in blueslip, and if
|
||||
// so our stub should include that.
|
||||
if (message.toString().includes("exception")) {
|
||||
message = message.toString();
|
||||
} else {
|
||||
throw new Error("message should be string: " + message);
|
||||
}
|
||||
}
|
||||
lib.test_logs[name].push({message, more_info, cause});
|
||||
const matched_error_message = lib.test_data[name].some((x) => x.message === message);
|
||||
const exact_match_fail = !matched_error_message;
|
||||
if (exact_match_fail) {
|
||||
const error = new Error(
|
||||
`Invalid ${name} message: "${message}".`,
|
||||
cause === undefined ? {} : {cause},
|
||||
);
|
||||
error.blueslip = true;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
||||
module.exports = make_zblueslip();
|
||||
98
web/tests/lib/zjquery.cjs
Normal file
98
web/tests/lib/zjquery.cjs
Normal file
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
/*
|
||||
When using zjquery, the first call to $("#foo")
|
||||
returns a new instance of the FakeJQuery pseudoclass,
|
||||
and then subsequent calls to $("#foo") get the
|
||||
same instance.
|
||||
*/
|
||||
const {default_element, FakeJQuery} = require("./zjquery_element.cjs");
|
||||
const FakeEvent = require("./zjquery_event.cjs");
|
||||
|
||||
function verify_selector_for_zulip(selector) {
|
||||
const is_valid =
|
||||
"<#.".includes(selector[0]) ||
|
||||
selector === "window-stub" ||
|
||||
selector === "document-stub" ||
|
||||
selector === "body" ||
|
||||
selector === "html" ||
|
||||
selector === ":root" ||
|
||||
selector.location ||
|
||||
selector.includes("#") ||
|
||||
selector.includes(".") ||
|
||||
(selector.includes("[") && selector.indexOf("]") >= selector.indexOf("["));
|
||||
|
||||
assert.ok(
|
||||
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').
|
||||
/^[ A-Za-z]+$/.test(selector)
|
||||
? "Selector too broad! Use id, class or attributes of target instead."
|
||||
: `Invalid selector: ${selector}. Use $.create() maybe?`,
|
||||
);
|
||||
}
|
||||
|
||||
function make_zjquery() {
|
||||
const results = new Map();
|
||||
|
||||
const zjquery = function (arg, arg2) {
|
||||
assert.ok(typeof arg !== "function", "zjquery does not support $(callback)");
|
||||
|
||||
// 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 (typeof arg === "object" && "to_$" in arg) {
|
||||
assert.equal(typeof arg.to_$, "function");
|
||||
return arg.to_$();
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
arg2,
|
||||
undefined,
|
||||
"We only use one-argument variations of $(...) in Zulip code.",
|
||||
);
|
||||
|
||||
const selector = arg;
|
||||
|
||||
/* istanbul ignore if */
|
||||
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 (!results.has(selector)) {
|
||||
results.set(selector, [default_element(selector)]);
|
||||
}
|
||||
return new FakeJQuery(results.get(selector));
|
||||
};
|
||||
|
||||
zjquery.create = function (selector, opts) {
|
||||
assert.ok(!results.has(selector), "You already created an object with this name!!");
|
||||
const elements = opts?.elements ?? [default_element(selector)];
|
||||
results.set(selector, elements);
|
||||
|
||||
return new FakeJQuery(elements);
|
||||
};
|
||||
|
||||
zjquery.set_results = (selector, elements) => zjquery.create(selector, {elements});
|
||||
|
||||
zjquery.Event = FakeEvent;
|
||||
|
||||
zjquery.reset_selector = (selector) => {
|
||||
results.delete(selector);
|
||||
};
|
||||
|
||||
zjquery.clear_all_elements = function () {
|
||||
results.clear();
|
||||
};
|
||||
|
||||
return zjquery;
|
||||
}
|
||||
|
||||
exports.$ = Object.freeze(make_zjquery());
|
||||
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);
|
||||
}
|
||||
};
|
||||
12
web/tests/lib/zjquery_event.cjs
Normal file
12
web/tests/lib/zjquery_event.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
class FakeEvent {
|
||||
constructor(type, props) {
|
||||
this.type = type;
|
||||
Object.assign(this, props);
|
||||
}
|
||||
preventDefault() {}
|
||||
stopPropagation() {}
|
||||
}
|
||||
|
||||
module.exports = FakeEvent;
|
||||
12
web/tests/lib/zpage_billing_params.cjs
Normal file
12
web/tests/lib/zpage_billing_params.cjs
Normal file
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
exports.page_params = {};
|
||||
|
||||
exports.reset = () => {
|
||||
for (const field in exports.page_params) {
|
||||
if (Object.hasOwn(exports.page_params, field)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete exports.page_params[field];
|
||||
}
|
||||
}
|
||||
};
|
||||
22
web/tests/lib/zpage_params.cjs
Normal file
22
web/tests/lib/zpage_params.cjs
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
exports.page_params = {};
|
||||
|
||||
exports.reset = () => {
|
||||
for (const field in exports.page_params) {
|
||||
if (Object.hasOwn(exports.page_params, field)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete exports.page_params[field];
|
||||
}
|
||||
}
|
||||
exports.page_params.request_language = "en";
|
||||
exports.page_params.translation_data = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => `translated: ${key}`,
|
||||
getOwnPropertyDescriptor: (_target, _key) => ({configurable: true}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
exports.reset();
|
||||
Reference in New Issue
Block a user