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:
474
web/tests/hashchange.test.cjs
Normal file
474
web/tests/hashchange.test.cjs
Normal file
@@ -0,0 +1,474 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {make_stream} = require("./lib/example_stream.cjs");
|
||||
const {mock_esm, set_global, zrequire} = require("./lib/namespace.cjs");
|
||||
const {run_test, noop} = require("./lib/test.cjs");
|
||||
const blueslip = require("./lib/zblueslip.cjs");
|
||||
const {$} = require("./lib/zjquery.cjs");
|
||||
const {page_params} = require("./lib/zpage_params.cjs");
|
||||
|
||||
let $window_stub;
|
||||
set_global("to_$", () => $window_stub);
|
||||
|
||||
set_global("document", "document-stub");
|
||||
const history = set_global("history", {state: null});
|
||||
|
||||
const admin = mock_esm("../src/admin");
|
||||
const drafts_overlay_ui = mock_esm("../src/drafts_overlay_ui");
|
||||
const info_overlay = mock_esm("../src/info_overlay");
|
||||
const message_viewport = mock_esm("../src/message_viewport");
|
||||
const overlays = mock_esm("../src/overlays");
|
||||
const popovers = mock_esm("../src/popovers");
|
||||
const recent_view_ui = mock_esm("../src/recent_view_ui");
|
||||
const settings = mock_esm("../src/settings");
|
||||
mock_esm("../src/settings_data", {
|
||||
user_can_create_public_streams: () => true,
|
||||
});
|
||||
const sidebar_ui = mock_esm("../src/sidebar_ui");
|
||||
const spectators = mock_esm("../src/spectators", {
|
||||
login_to_access() {},
|
||||
});
|
||||
const stream_settings_ui = mock_esm("../src/stream_settings_ui");
|
||||
const ui_util = mock_esm("../src/ui_util");
|
||||
const ui_report = mock_esm("../src/ui_report");
|
||||
set_global("favicon", {});
|
||||
|
||||
const browser_history = zrequire("browser_history");
|
||||
const people = zrequire("people");
|
||||
const hash_util = zrequire("hash_util");
|
||||
const hashchange = zrequire("hashchange");
|
||||
const message_view = zrequire("../src/message_view");
|
||||
const stream_data = zrequire("stream_data");
|
||||
const {Filter} = zrequire("../src/filter");
|
||||
const {initialize_user_settings} = zrequire("user_settings");
|
||||
|
||||
const user_settings = {};
|
||||
initialize_user_settings({user_settings});
|
||||
|
||||
const devel_id = 100;
|
||||
const devel = make_stream({
|
||||
name: "devel",
|
||||
stream_id: devel_id,
|
||||
color: "blue",
|
||||
subscribed: true,
|
||||
});
|
||||
stream_data.add_sub_for_tests(devel);
|
||||
|
||||
run_test("terms_round_trip", () => {
|
||||
let terms;
|
||||
let hash;
|
||||
let narrow;
|
||||
|
||||
terms = [
|
||||
{operator: "channel", operand: devel_id.toString()},
|
||||
{operator: "topic", operand: "algol"},
|
||||
];
|
||||
hash = hash_util.search_terms_to_hash(terms);
|
||||
assert.equal(hash, "#narrow/channel/100-devel/topic/algol");
|
||||
|
||||
narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [
|
||||
{operator: "channel", operand: devel_id.toString(), negated: false},
|
||||
{operator: "topic", operand: "algol", negated: false},
|
||||
]);
|
||||
|
||||
terms = [
|
||||
{operator: "channel", operand: devel_id.toString()},
|
||||
{operator: "topic", operand: "visual c++", negated: true},
|
||||
];
|
||||
hash = hash_util.search_terms_to_hash(terms);
|
||||
assert.equal(hash, "#narrow/channel/100-devel/-topic/visual.20c.2B.2B");
|
||||
|
||||
narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [
|
||||
{operator: "channel", operand: devel_id.toString(), negated: false},
|
||||
{operator: "topic", operand: "visual c++", negated: true},
|
||||
]);
|
||||
|
||||
// test new encodings, where we have a stream id
|
||||
const florida_id = 987;
|
||||
const florida_stream = make_stream({
|
||||
name: "Florida, USA",
|
||||
stream_id: florida_id,
|
||||
});
|
||||
stream_data.add_sub_for_tests(florida_stream);
|
||||
terms = [{operator: "channel", operand: florida_id.toString()}];
|
||||
hash = hash_util.search_terms_to_hash(terms);
|
||||
assert.equal(hash, "#narrow/channel/987-Florida.2C-USA");
|
||||
narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [
|
||||
{operator: "channel", operand: florida_id.toString(), negated: false},
|
||||
]);
|
||||
});
|
||||
|
||||
run_test("terms_trailing_slash", () => {
|
||||
const hash = "#narrow/channel/100-devel/topic/algol/";
|
||||
const narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [
|
||||
{operator: "channel", operand: devel_id.toString(), negated: false},
|
||||
{operator: "topic", operand: "algol", negated: false},
|
||||
]);
|
||||
});
|
||||
|
||||
run_test("people_slugs", () => {
|
||||
let terms;
|
||||
let hash;
|
||||
let narrow;
|
||||
|
||||
const alice = {
|
||||
email: "alice@example.com",
|
||||
user_id: 42,
|
||||
full_name: "Alice Smith",
|
||||
};
|
||||
|
||||
people.add_active_user(alice, "server_events");
|
||||
terms = [{operator: "sender", operand: alice.user_id}];
|
||||
hash = hash_util.search_terms_to_hash(terms);
|
||||
assert.equal(hash, "#narrow/sender/42-Alice-Smith");
|
||||
narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [{operator: "sender", operand: alice.user_id, negated: false}]);
|
||||
|
||||
terms = [{operator: "dm", operand: [alice.user_id]}];
|
||||
hash = hash_util.search_terms_to_hash(terms);
|
||||
assert.equal(hash, "#narrow/dm/42-Alice-Smith");
|
||||
narrow = hash_util.parse_narrow(hash.split("/"));
|
||||
assert.deepEqual(narrow, [{operator: "dm", operand: [alice.user_id], negated: false}]);
|
||||
});
|
||||
|
||||
function test_helper({override, override_rewire, change_tab}) {
|
||||
let events = [];
|
||||
let narrow_terms;
|
||||
|
||||
function stub(module, func_name) {
|
||||
module[func_name] = () => {
|
||||
events.push([module, func_name]);
|
||||
};
|
||||
}
|
||||
|
||||
function stub_with_args(module, func_name) {
|
||||
module[func_name] = (...args) => {
|
||||
events.push([module, func_name, args]);
|
||||
};
|
||||
}
|
||||
|
||||
stub_with_args(admin, "launch");
|
||||
stub(admin, "build_page");
|
||||
stub(drafts_overlay_ui, "launch");
|
||||
stub(message_viewport, "stop_auto_scrolling");
|
||||
stub(overlays, "close_for_hash_change");
|
||||
stub(settings, "launch");
|
||||
stub(settings, "build_page");
|
||||
stub(stream_settings_ui, "launch");
|
||||
stub(ui_util, "blur_active_element");
|
||||
stub(ui_report, "error");
|
||||
stub(spectators, "login_to_access");
|
||||
|
||||
if (change_tab) {
|
||||
override_rewire(message_view, "show", (terms) => {
|
||||
narrow_terms = terms;
|
||||
events.push("message_view.show");
|
||||
});
|
||||
|
||||
override(info_overlay, "show", (name) => {
|
||||
events.push("info: " + name);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
clear_events() {
|
||||
events = [];
|
||||
},
|
||||
assert_events(expected_events) {
|
||||
assert.deepEqual(events, expected_events);
|
||||
},
|
||||
get_narrow_terms: () => narrow_terms,
|
||||
};
|
||||
}
|
||||
|
||||
run_test("hash_interactions", ({override, override_rewire}) => {
|
||||
$window_stub = $.create("window-stub");
|
||||
override(user_settings, "web_home_view", "recent");
|
||||
|
||||
const helper = test_helper({override, override_rewire, change_tab: true});
|
||||
|
||||
let recent_view_ui_shown = false;
|
||||
override(recent_view_ui, "show", () => {
|
||||
recent_view_ui_shown = true;
|
||||
});
|
||||
let hide_all_called = false;
|
||||
override(popovers, "hide_all", () => {
|
||||
hide_all_called = true;
|
||||
});
|
||||
let sidebar_hide_all_called = false;
|
||||
override(sidebar_ui, "hide_all", () => {
|
||||
sidebar_hide_all_called = true;
|
||||
});
|
||||
window.location.hash = "#unknown_hash";
|
||||
|
||||
browser_history.clear_for_testing();
|
||||
hashchange.initialize();
|
||||
// If it's an unknown hash it should show the home view.
|
||||
assert.equal(recent_view_ui_shown, true);
|
||||
assert.equal(hide_all_called, true);
|
||||
assert.equal(sidebar_hide_all_called, true);
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
]);
|
||||
|
||||
window.location.hash = "#feed";
|
||||
hide_all_called = false;
|
||||
sidebar_hide_all_called = false;
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
assert.equal(hide_all_called, true);
|
||||
assert.equal(sidebar_hide_all_called, true);
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
"message_view.show",
|
||||
]);
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
"message_view.show",
|
||||
]);
|
||||
|
||||
// Test old "#recent_topics" hash redirects to "#recent".
|
||||
recent_view_ui_shown = false;
|
||||
window.location.hash = "#recent_topics";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
assert.equal(recent_view_ui_shown, true);
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
]);
|
||||
assert.equal(window.location.hash, "#recent");
|
||||
|
||||
const denmark_id = 1;
|
||||
stream_data.add_sub_for_tests(
|
||||
make_stream({
|
||||
subscribed: true,
|
||||
name: "Denmark",
|
||||
stream_id: denmark_id,
|
||||
}),
|
||||
);
|
||||
window.location.hash = "#narrow/channel/Denmark";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
"message_view.show",
|
||||
]);
|
||||
let terms = helper.get_narrow_terms();
|
||||
assert.equal(terms[0].operand, denmark_id.toString());
|
||||
|
||||
window.location.hash = "#narrow";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
"message_view.show",
|
||||
]);
|
||||
terms = helper.get_narrow_terms();
|
||||
assert.equal(terms.length, 0);
|
||||
|
||||
page_params.is_spectator = true;
|
||||
|
||||
window.location.hash = "#narrow/is/resolved/has/reaction";
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
"message_view.show",
|
||||
]);
|
||||
terms = helper.get_narrow_terms();
|
||||
assert.equal(terms.length, 2);
|
||||
|
||||
// Test a narrow that spectators are not permitted to access.
|
||||
window.location.hash = "#narrow/is/resolved/is/unread";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([[spectators, "login_to_access"]]);
|
||||
|
||||
page_params.is_spectator = false;
|
||||
|
||||
// Test an invalid narrow hash
|
||||
window.location.hash = "#narrow/foo.foo";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
[ui_report, "error"],
|
||||
]);
|
||||
|
||||
window.location.hash = "#channels/subscribed";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[stream_settings_ui, "launch"],
|
||||
]);
|
||||
|
||||
recent_view_ui_shown = false;
|
||||
window.location.hash = "#reload:send_after_reload=0...";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([]);
|
||||
// If it's reload hash it shouldn't show the home view.
|
||||
assert.equal(recent_view_ui_shown, false);
|
||||
|
||||
window.location.hash = "#keyboard-shortcuts/whatever";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([[overlays, "close_for_hash_change"], "info: keyboard-shortcuts"]);
|
||||
|
||||
window.location.hash = "#message-formatting/whatever";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([[overlays, "close_for_hash_change"], "info: message-formatting"]);
|
||||
|
||||
window.location.hash = "#search-operators/whatever";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([[overlays, "close_for_hash_change"], "info: search-operators"]);
|
||||
|
||||
window.location.hash = "#drafts";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[drafts_overlay_ui, "launch"],
|
||||
]);
|
||||
|
||||
window.location.hash = "#settings/alert-words";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[settings, "build_page"],
|
||||
[admin, "build_page"],
|
||||
[settings, "launch"],
|
||||
]);
|
||||
|
||||
window.location.hash = "#organization/users/active";
|
||||
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[settings, "build_page"],
|
||||
[admin, "build_page"],
|
||||
[admin, "launch", ["users", "active"]],
|
||||
]);
|
||||
|
||||
let fixed_url;
|
||||
override(history, "replaceState", (_state, _title, url) => {
|
||||
fixed_url = url;
|
||||
window.location.hash = new URL(url).hash;
|
||||
});
|
||||
|
||||
window.location.hash = "#organization/user-list-admin";
|
||||
|
||||
// Check whether `user-list-admin` is redirect to `users`, we
|
||||
// cannot test the exact hashchange here, since the section url
|
||||
// takes effect in `admin.launch` and that's why we're checking
|
||||
// the arguments passed to `admin.launch`.
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
assert.equal(fixed_url, "http://zulip.zulipdev.com/#organization/users/active");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[settings, "build_page"],
|
||||
[admin, "build_page"],
|
||||
[admin, "launch", ["users", "active"]],
|
||||
]);
|
||||
|
||||
helper.clear_events();
|
||||
browser_history.exit_overlay();
|
||||
|
||||
helper.assert_events([[ui_util, "blur_active_element"]]);
|
||||
});
|
||||
|
||||
run_test("update_hash_to_match_filter", ({override, override_rewire}) => {
|
||||
const helper = test_helper({override, override_rewire});
|
||||
|
||||
let terms = [{operator: "is", operand: "dm"}];
|
||||
|
||||
blueslip.expect("error", "browser does not support pushState");
|
||||
message_view.update_hash_to_match_filter(new Filter(terms));
|
||||
|
||||
helper.assert_events([[message_viewport, "stop_auto_scrolling"]]);
|
||||
assert.equal(window.location.hash, "#narrow/is/dm");
|
||||
|
||||
let url_pushed;
|
||||
override(history, "pushState", (_state, _title, url) => {
|
||||
url_pushed = url;
|
||||
});
|
||||
|
||||
terms = [{operator: "is", operand: "starred"}];
|
||||
|
||||
helper.clear_events();
|
||||
message_view.update_hash_to_match_filter(new Filter(terms));
|
||||
helper.assert_events([[message_viewport, "stop_auto_scrolling"]]);
|
||||
assert.equal(url_pushed, "http://zulip.zulipdev.com/#narrow/is/starred");
|
||||
|
||||
terms = [{operator: "is", operand: "starred", negated: true}];
|
||||
|
||||
helper.clear_events();
|
||||
message_view.update_hash_to_match_filter(new Filter(terms));
|
||||
helper.assert_events([[message_viewport, "stop_auto_scrolling"]]);
|
||||
assert.equal(url_pushed, "http://zulip.zulipdev.com/#narrow/-is/starred");
|
||||
});
|
||||
|
||||
run_test("fail_incorrectly_cased_URL", ({override, override_rewire}) => {
|
||||
browser_history.clear_for_testing();
|
||||
override(popovers, "hide_all", noop);
|
||||
override(sidebar_ui, "hide_all", noop);
|
||||
const helper = test_helper({override, override_rewire, change_tab: false});
|
||||
|
||||
// We can receive URLs which contain operators that
|
||||
// are not cased correctly. We don't have to handle them
|
||||
// since this is not a good reason to increase the types
|
||||
// of URLs that are valid on a Zulip realm.
|
||||
window.location.hash = "#narrow/chAnnel/4-Denmark/topic/PLOTS/with/99";
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
[ui_report, "error"],
|
||||
]);
|
||||
|
||||
window.location.hash = "#narrow/channel/4-Denmark/tOPic/PLOTS/with/99";
|
||||
helper.clear_events();
|
||||
$window_stub.trigger("hashchange");
|
||||
helper.assert_events([
|
||||
[overlays, "close_for_hash_change"],
|
||||
[message_viewport, "stop_auto_scrolling"],
|
||||
[ui_report, "error"],
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user