"use strict"; const {strict: assert} = require("assert"); const {mock_esm, set_global, zrequire} = require("./lib/namespace"); const {run_test} = require("./lib/test"); const blueslip = require("./lib/zblueslip"); const $ = require("./lib/zjquery"); const {user_settings} = require("./lib/zpage_params"); let $window_stub; set_global("to_$", () => $window_stub); set_global("document", "document-stub"); const history = set_global("history", {}); const admin = mock_esm("../src/admin"); const drafts = mock_esm("../src/drafts"); const info_overlay = mock_esm("../src/info_overlay"); const message_viewport = mock_esm("../src/message_viewport"); const narrow = mock_esm("../src/narrow"); const overlays = mock_esm("../src/overlays"); const popovers = mock_esm("../src/popovers"); const recent_topics_ui = mock_esm("../src/recent_topics_ui"); const settings = mock_esm("../src/settings"); 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"); mock_esm("../src/top_left_corner", { handle_narrow_deactivated() {}, }); set_global("favicon", {}); const browser_history = zrequire("browser_history"); const people = zrequire("people"); const hash_util = zrequire("hash_util"); const hashchange = zrequire("hashchange"); const stream_data = zrequire("stream_data"); run_test("operators_round_trip", () => { let operators; let hash; let narrow; operators = [ {operator: "stream", operand: "devel"}, {operator: "topic", operand: "algol"}, ]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/stream/devel/topic/algol"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [ {operator: "stream", operand: "devel", negated: false}, {operator: "topic", operand: "algol", negated: false}, ]); operators = [ {operator: "stream", operand: "devel"}, {operator: "topic", operand: "visual c++", negated: true}, ]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/stream/devel/-topic/visual.20c.2B.2B"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [ {operator: "stream", operand: "devel", negated: false}, {operator: "topic", operand: "visual c++", negated: true}, ]); // test new encodings, where we have a stream id const florida_stream = { name: "Florida, USA", stream_id: 987, }; stream_data.add_sub(florida_stream); operators = [{operator: "stream", operand: "Florida, USA"}]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/stream/987-Florida.2C-USA"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [{operator: "stream", operand: "Florida, USA", negated: false}]); }); run_test("operators_trailing_slash", () => { const hash = "#narrow/stream/devel/topic/algol/"; const narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [ {operator: "stream", operand: "devel", negated: false}, {operator: "topic", operand: "algol", negated: false}, ]); }); run_test("people_slugs", () => { let operators; let hash; let narrow; const alice = { email: "alice@example.com", user_id: 42, full_name: "Alice Smith", }; people.add_active_user(alice); operators = [{operator: "sender", operand: "alice@example.com"}]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/sender/42-Alice-Smith"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [{operator: "sender", operand: "alice@example.com", negated: false}]); operators = [{operator: "dm", operand: "alice@example.com"}]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/dm/42-Alice-Smith"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [{operator: "dm", operand: "alice@example.com", negated: false}]); // Even though we renamed "pm-with" to "dm", preexisting // links/URLs with "pm-with" operator are handled correctly. operators = [{operator: "pm-with", operand: "alice@example.com"}]; hash = hash_util.operators_to_hash(operators); assert.equal(hash, "#narrow/pm-with/42-Alice-Smith"); narrow = hash_util.parse_narrow(hash.split("/")); assert.deepEqual(narrow, [{operator: "pm-with", operand: "alice@example.com", negated: false}]); }); function test_helper({override, change_tab}) { let events = []; let narrow_terms; function stub(module, func_name) { module[func_name] = () => { events.push([module, func_name]); }; } stub(admin, "launch"); stub(admin, "build_page"); stub(drafts, "launch"); stub(message_viewport, "stop_auto_scrolling"); stub(narrow, "deactivate"); 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"); if (change_tab) { override(narrow, "activate", (terms) => { narrow_terms = terms; events.push("narrow.activate"); }); 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}) => { $window_stub = $.create("window-stub"); user_settings.default_view = "recent_topics"; const helper = test_helper({override, change_tab: true}); let recent_topics_ui_shown = false; override(recent_topics_ui, "show", () => { recent_topics_ui_shown = true; }); let hide_all_called = false; override(popovers, "hide_all", () => { 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 default view. assert.equal(recent_topics_ui_shown, true); assert.equal(hide_all_called, true); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], ]); window.location.hash = "#all_messages"; hide_all_called = false; helper.clear_events(); $window_stub.trigger("hashchange"); assert.equal(hide_all_called, true); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], [narrow, "deactivate"], ]); helper.clear_events(); $window_stub.trigger("hashchange"); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], [narrow, "deactivate"], ]); // Test old "#recent_topics" hash redirects to "#recent". recent_topics_ui_shown = false; window.location.hash = "#recent_topics"; helper.clear_events(); $window_stub.trigger("hashchange"); assert.equal(recent_topics_ui_shown, true); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], ]); assert.equal(window.location.hash, "#recent"); window.location.hash = "#narrow/stream/Denmark"; helper.clear_events(); $window_stub.trigger("hashchange"); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], "narrow.activate", ]); let terms = helper.get_narrow_terms(); assert.equal(terms[0].operand, "Denmark"); window.location.hash = "#narrow"; helper.clear_events(); $window_stub.trigger("hashchange"); helper.assert_events([ [overlays, "close_for_hash_change"], [message_viewport, "stop_auto_scrolling"], "narrow.activate", ]); terms = helper.get_narrow_terms(); assert.equal(terms.length, 0); // 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 = "#streams/whatever"; helper.clear_events(); $window_stub.trigger("hashchange"); helper.assert_events([ [overlays, "close_for_hash_change"], [stream_settings_ui, "launch"], ]); recent_topics_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 default view. assert.equal(recent_topics_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, "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/user-list-admin"; helper.clear_events(); $window_stub.trigger("hashchange"); helper.assert_events([ [overlays, "close_for_hash_change"], [settings, "build_page"], [admin, "build_page"], [admin, "launch"], ]); helper.clear_events(); browser_history.exit_overlay(); helper.assert_events([[ui_util, "blur_active_element"]]); }); run_test("save_narrow", ({override}) => { const helper = test_helper({override}); let operators = [{operator: "is", operand: "dm"}]; blueslip.expect("error", "browser does not support pushState"); hashchange.save_narrow(operators); 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; }); operators = [{operator: "is", operand: "starred"}]; helper.clear_events(); hashchange.save_narrow(operators); helper.assert_events([[message_viewport, "stop_auto_scrolling"]]); assert.equal(url_pushed, "http://zulip.zulipdev.com/#narrow/is/starred"); });