Compare commits

..

240 Commits

Author SHA1 Message Date
Greg Price
3ff2bcf62a shared: Bump version to 0.0.10. 2022-03-30 21:06:37 -07:00
Anders Kaseorg
7de1e7c477 changelog: Remove broken link.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-30 20:37:31 -07:00
Steve Howell
b7f670f5a0 markdown: Extract set_linkifier_regexes.
This is definitely better than having linkifiers
reach directly into marked.js, but there is
probably further improvement we can do here
to clean up how these regexes get set.

This introduces a circular dependency between
markdown.js and linkifiers.js, but we will
soon break it in the other direction.
2022-03-30 14:31:00 -07:00
Steve Howell
2a240d3e19 markdown: Move handleLinkifier.
All the other handleFoo functions followed this
convention.
2022-03-30 14:31:00 -07:00
Steve Howell
efdf2c8fe3 linkifiers: Avoid parallel data structure.
We can pretty easily work with a map in the
two places that we ever relied on an array.
2022-03-30 14:31:00 -07:00
Steve Howell
71c12e313c markdown: Fix overly loose regex for previews.
Fortunately, the only impact of this bug was that
we would unnecessarily wait for the server to render
the markdown if we got false matches.
2022-03-30 14:31:00 -07:00
Steve Howell
da6d687215 markdown: Extract contains_preview_link.
I also clean up some variable names, comments, and
idioms.
2022-03-30 14:31:00 -07:00
Steve Howell
58799c6ca1 markdown: Extract contains_problematic_linkifier.
Note we now avoid linkifier checks for the case that a message
contains more obvious backend-only syntax such as attachments.

The next commit will eliminate the ugly early-return.
2022-03-30 14:31:00 -07:00
Steve Howell
8d9e6d6b87 markdown: Clean up API for future reuse.
This gets us closer to having an API that can
be used my mobile.

The parse() function becomes a subset of
apply_markdown() that is no longer coupled
to the shape of a webapp object, and it can
be supplied with a new helper_config for each
invocation. Mobile will likely call this directly.

The setup() function becomes a subset of
initialize() that allows you to set up the
parser **before** having to build any kind of
message-specific helpers. Mobile will likely
call this directly.

The webapp continues to call these functions,
which are now thin wrappers:

    * apply_markdown (wrapping parse)
    * initialize (wrapping setup)

Note we still have several other problems to
solve before mobile can use this code, but we
introduce this now so that we can get a head
start on prototyping and unit testing.

Also, this commit does not address the fact
that contains_backend_only_syntax() is still
bound to the webapp config.
2022-03-30 14:31:00 -07:00
Anders Kaseorg
935cb605a5 puppet: Do not ensure Chrony is running.
Commit f6d27562fa (#21564) tried to
ensure Chrony is running, which fails in containers where Chrony
doesn’t have permission to update the host clock.

The Debian package should still attempt to start it, and Puppet should
still restart it when chrony.conf is modified.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-30 11:37:54 -07:00
Sahil Batra
ca38b33346 settings: Fix push notifications tooltip being incorrectly shown.
We were showing the push notifications tooltip in user default
settings section even if the push notifications were configured
on the server.

The bug was because the setting value was undefined in the template
used for user default settings section, so this commit fixes the bug
by correctly passing the setting value to relevant template file.

Fixes #21602.
2022-03-30 11:31:29 -07:00
Lauryn Menard
0008a76703 tests: Remove ignored stream_name parameter from test.
Removes unnecessary `stream_name` parameter from
`test_stream_permission_changes_updates_updates_attachments`.
2022-03-30 11:30:31 -07:00
Tim Abbott
d9bf8baca1 tools: Add per-repository commit counts in contributions tool.
This makes the output nice enough to include in the blog post.
2022-03-29 14:13:17 -07:00
Tim Abbott
46b19fe8bd mailmap: Add entries to deduplicate more contributors. 2022-03-29 12:13:21 -07:00
Tim Abbott
c1103e4c7b i18n: Fix missing translation tag in footer. 2022-03-29 10:04:35 -07:00
Tim Abbott
c8dba33408 docs: Fix broken link in changelog. 2022-03-29 09:52:07 -07:00
Tim Abbott
6ea9947991 docs: Run prettier on changelog. 2022-03-29 09:24:06 -07:00
Tim Abbott
12e8f0f5ea version: Update version following 5.0 release. 2022-03-29 08:36:41 -07:00
Tim Abbott
d308c694ba Release Zulip Server 5.0. 2022-03-29 08:13:34 -07:00
Tim Abbott
836529f826 i18n: Update translation data from Transifex. 2022-03-29 08:11:21 -07:00
jai2201
20e6315705 streams: Fix empty description in stream change notifications.
Send 'No description' text when either the old or new stream
description is empty, to avoid odd looking notifications.
2022-03-29 01:39:55 -07:00
Tim Abbott
c789097184 streams: Fix empty description handling in new stream notifications.
"No description." is conveniniently an existing translated string, in
addition to being the right content for this setting.
2022-03-29 01:39:55 -07:00
jai2201
52cf557d2b openapi: Allow empty value of stream's description.
This is important because otherwise tests that change a stream
description to the empty string don't work.
2022-03-29 01:39:55 -07:00
Greg Price
35ee5679d6 mailmap: Add entries for strifel and m-e-l-u-h-a-n.
In each case guessing with the name they've used more often.
2022-03-29 00:27:23 -07:00
Greg Price
f054d217c2 mailmap: Add entry for Shlok Patel.
Guessing with the name that looks like a full name, rather than
a username; which is also the form they've used more often.
2022-03-29 00:27:23 -07:00
Greg Price
fecc557836 mailmap: Add entry for Parth.
Guessing with the capitalized form, which is also the one they've
used slightly more often.
2022-03-29 00:27:23 -07:00
Greg Price
e06e05acac mailmap: Add entry for Kevin Scott.
Guessing with the fuller form of the name, which is also what
appears in their email address.
2022-03-29 00:27:23 -07:00
Greg Price
a907c10509 mailmap: Add entries for BIKI DAS, Palash Raghuwanshi, and Yogesh Sirsat.
Based on the names on their respective chat.zulip.org profiles.
2022-03-29 00:27:23 -07:00
Greg Price
2455077346 mailmap: Add entry for Sayam Samal.
Based on the name found on their chat.zulip.org profile.
2022-03-29 00:27:23 -07:00
Greg Price
245288df1e mailmap: Add entries for Lauryn, Mateusz, Austin, Alya, and Eeshan.
Canonicalizing on the zulip.com email address for each of them.
2022-03-29 00:27:23 -07:00
Greg Price
244f34b66e mailmap: Add entry for Jai soni.
Based on the non-noreply.github.com email address, and the name
found on their chat.zulip.org profile.
2022-03-29 00:27:23 -07:00
Alya Abbott
2c558750de portico: Add web-public steams and emoji statuses to /for/X pages. 2022-03-28 23:27:33 -07:00
Tim Abbott
13da2c2fb7 total-contributions: Clone repositories if already available.
This makes it more convenient to manage.
2022-03-28 23:01:58 -07:00
Tim Abbott
a7f13ba723 total-contributions: Support all repositories and improve output. 2022-03-28 23:01:58 -07:00
Tim Abbott
7cbe9665b6 total-contributions: Exclude dependabot commits. 2022-03-28 23:01:58 -07:00
Tim Abbott
a234fe9c4c total-contributions: Use commits by date, not by release. 2022-03-28 23:01:58 -07:00
Tim Abbott
6b5ce3579c total-contributions: Do a fetch for zulip/zulip as well.
This avoids producing stale output by having a consistent algorithm
for all repositories.
2022-03-28 23:01:58 -07:00
Alya Abbott
f04fb51ecc help: Rewrite pages on inviting users and related permissions.
Fixes #21520.

Co-authored-by: Lauryn Menard <lauryn@zulip.com>
2022-03-28 17:00:10 -07:00
Lauryn Menard
cbfe2707f4 help_docs: Clarify relative link text for Subscribed streams tab. 2022-03-28 15:13:39 -07:00
Lauryn Menard
1b55ff79c9 help_docs: Remove last instance of 'Zulips' in help center docs. 2022-03-28 15:13:08 -07:00
Lauryn Menard
e4d051b5f4 help_docs: Fix help center link on Zulip features page.
Fixes a link to the help center markdown formatting documentation
about quotes.
2022-03-28 15:12:53 -07:00
Greg Price
56b6b71236 notification_sounds: Add "Chime", the new Zulip sound used on Android
This is the same sound that since zulip/zulip-mobile#5221 has been
the default notification sound for Zulip on Android.

The actual sound was created by Anders.

Co-authored-by: Anders Kaseorg <anders@zulip.com>
2022-03-28 12:56:54 -07:00
Lauryn Menard
012e2d29a2 help_docs: Clarify header and text for unmuting topics. 2022-03-28 10:33:02 -07:00
Alex Vandiver
ca506f71dc push_notifications: Increase severity of APNs ConnectionError.
This has only happened when our APNs certificate expired; logging at
the error level ensures that this shows up in Sentry.
2022-03-25 18:12:14 -07:00
Alya Abbott
20bb1b70d1 docs: Clarify stages of PR review. 2022-03-25 18:11:18 -07:00
Alya Abbott
2a1e08759b portico: Add Asciidoctor case study. 2022-03-25 17:51:15 -07:00
Alex Vandiver
f6d27562fa puppet: Configure chrony to use AWS-local NTP sources.
This prevents hosts from spewing traffic to random hosts across the
Internet.
2022-03-25 17:07:53 -07:00
Alex Vandiver
5e128e7cad puppet: Extract the wal-g configuration from the backups.
This will allow it to be used for monitoring, to check the state in S3
rather than just trusting the backups when they said they ran.
2022-03-25 17:05:30 -07:00
Lauryn Menard
8d242f3467 help_docs: Update edit history documentation for messages.
`disable-message-edit-history`: Remove text about EDITED label
and link to `view-a-messages-edit-history` instead.

`edit-or-delete-a-message`: Reformat 'EDITED' and '(deleted)'
to be bold instead of using backticks. Make link to view
edit history clearer.

Note that the text used in the `OpenGraphTest` in
`test_middleware.py` had to be updated for the changes to
`disable-message-edit-history`.
2022-03-25 17:03:53 -07:00
Aman Agrawal
41b7b3d7a4 css: Adjust vertical alignment of globe and lock icons. 2022-03-25 17:02:18 -07:00
Aman Agrawal
9dd8d13e0c compose: Add more space around compose stream privacy icons.
This is especially to add more space around the globe icon since
it wider than lock icon, so the previously set padding for the
stream icon has to be increased.
2022-03-25 17:02:18 -07:00
Lauryn Menard
9ce924a3a6 help_docs: Document auto-notifications for general stream updates.
Adds a note about auto-notification messages that are sent when
these general settings are changed for a stream: name, description,
access and posting permissions, and message retention.
2022-03-25 11:00:28 -07:00
Anders Kaseorg
2d71858380 black: Bump target-version to py37.
This lets Black support a Python 3.7 async comprehension syntax, but
has no effect on our current code.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-25 10:45:37 -07:00
Anders Kaseorg
55882fb343 python: Use modern set comprehension syntax.
Generated by pyupgrade.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-25 10:45:12 -07:00
Anders Kaseorg
1f68c73e66 supervisor: Update superseded super(C, self) syntax to superior super().
Generated by pyupgrade.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-25 10:45:12 -07:00
yogesh sirsat
078d966c64 modals: Refactor help_link_widget for confirmation modal.
Added class "help_link_widget" and applied existing css,
to `a` tag of help_link_widget.

Follow-up of #21508.
2022-03-25 10:43:37 -07:00
Alex Vandiver
eae4643cb4 message_edit: Ignore duplicates when re-muting new topic name.
This avoids an error when a user has already muted the new topic name.
We do this by ignoring duplicates, rather than catching the
IntegrityError, because this edit happens in a transaction, and that
would abort the transaction.
2022-03-24 21:27:11 -07:00
Alex Vandiver
141b0c4cec muting: Handle the case of a race muting the same user twice. 2022-03-24 21:27:11 -07:00
Alex Vandiver
781107308d muting: Add a flag to allow user duplicate mutes to silently succeed. 2022-03-24 21:27:11 -07:00
Anders Kaseorg
2762121162 python: Convert last type comments to annotations.
We had skipped these in #14693 so we could keep generating a friendly
error on Python 3.5, but we gave that up in #19801.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-24 20:32:39 -07:00
Anders Kaseorg
548dd5a402 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-24 19:16:52 -07:00
Anders Kaseorg
83c90c53df db: Fix types.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-24 19:16:52 -07:00
Tim Abbott
e3600900c0 migrations: Adjust related_name settings for ArchivedAttachment.
This is necessary for the migration 0386_fix_attachment_caches to run,
and likely makes more convenient any future parallel code interacting
with both Attachment and ArchivedAttachment.
2022-03-24 19:15:27 -07:00
Alex Vandiver
ab3196470f docs: Update release steps. 2022-03-24 18:05:40 -07:00
Tim Abbott
f9f111f950 message_edit: Only move muted topic records when moving whole topics.
Our original implementation of moving muted topic records when a topic
is moved took a shortcut of treating all change_later usage as
something with intent to move the whole topic.

This works OK when moving the whole topic via this interface, but not
when moving a last off-topic message in the topic.

Address this by changing the rule to match the existing
moved_all_visible_messages variable.
2022-03-24 17:48:52 -07:00
Tim Abbott
219e213c60 docs: Update changelog with changes since 5.0-rc1. 2022-03-24 17:44:58 -07:00
Tim Abbott
b493224cda management: Document logout_all_users interaction with API keys.
Fixes part of #19397, but I'm leaving that open since we'll want to
add an option to rotate all API keys for the target users.
2022-03-24 14:58:31 -07:00
Lauryn Menard
08dddeac7d core_docs: Update translated language count and help center links.
Updates `/for/` pages for the new translated language count, 23.

Also, updates any links to help center documentation that have
been changed.

Finally, updates `/for/events` text for a potentially confusing
English idiom.
2022-03-24 14:49:30 -07:00
Aman Agrawal
8d78f356e6 message_scroll: Use fadeOut effect to hide the scroll to bottom button.
We use `fadeOut` effect to hide the scroll to bottom button more
widely. We already use `fadeIn` effect to display the button
every time.

We deliberately don't use the `fadeOut` effect when doing
`make_compose_box_full_size` to avoid any button overlap with
compose for a short time.
2022-03-24 12:48:57 -07:00
yogesh sirsat
c5bb9cb08a settings_users: Confirmation modal for "Reactivate" user.
The implementation closely follows `handle_deactivation()`.

Using the same existing reactivate confirmation modal.

Also, this commit will also lead to open confirmation modal
to reactivate bots in settings > bots, and currently there is no
existing confirmation modal for deactivating bots.

This commit is a follow-up of #21436.
2022-03-24 12:39:12 -07:00
YashRE42
7e03ed9391 people: Extract _calc_user_and_other_ids from get_recipients.
We previously added support for showing the status_emoji to the PM
list, but we only supported individual PMs and not group PMs, this is
prep commit towards supporting group PMs.
2022-03-24 12:21:38 -07:00
YashRE42
72cf5bd90d people: Extract get_display_full_name from get_display_full_names.
We previously added support for showing the status_emoji to the PM
list, but we only supported individual PMs and not group PMs, this is
prep commit towards supporting group PMs.
2022-03-24 12:21:38 -07:00
yogesh sirsat
417766a3e3 modals: Fix margin for help_link_widget in confirmation modals.
The 5px margin is picked to match what we use in settings.
2022-03-24 12:11:47 -07:00
Lauryn Menard
bd936a837a help_docs: Update image viewer documentation for changes.
Updates the list of actions and buttons referenced in the help
center documentation for viewing images with lightbox.

Also, makes some minor corrections to the keyboard shortcut note.

Fixes #21527.
2022-03-24 11:48:19 -07:00
Lauryn Menard
182c00248d help_docs: Update various docs related to permissions.
Adds tab for web-public streams in documentation for setting
who can create new streams, as well as some text about why
this is limited to certain roles.

Removes list of actions that can be restricted to full members
due to maintainability concerns for that type of list in the
documentation and replaces it with a short descriptive text
explaining that many settings in Zulip support this restriction.
2022-03-24 11:37:35 -07:00
Tim Abbott
f3488c540b help: Tweak index to group edit restrictions. 2022-03-24 11:28:01 -07:00
Lauryn Menard
e16ede4622 help_docs: Update documentation for editing messages.
Updates various articles related to editing messages (settings,
edit history, etc) for changes in UI, adding undocumented
settings, and cleaning/linking documentation text and headers.
2022-03-24 11:26:45 -07:00
Alex Vandiver
330f0649d7 docs: Remove a stray extra word. 2022-03-24 11:14:50 -07:00
YashRE42
e88ca470ac right_sidebar: Apply left margin to unread counts.
This change was motivated by the addition of status emoji to the buddy
list. Previously there was no spacing between the status emoji and the
unread count, and as such, this commit adds a left margin to the
unread count.

The above change has an additional consequence, long user names such
as "Othello, the Moor of Venice" get truncated with ellipses, instead
of reaching to the edge of the unread counts (as they previously
would).
2022-03-24 11:07:37 -07:00
YashRE42
07df504c79 status_emoji: Bump margin-left on status emoji by 1 px.
While the 2px value that we had previously chosen looked alright for
most emoji (😀, 😃, etc) some emoji such as 🐙
used more of the width available to them and as such still looked too
close to the user name. As such this commit bumps the value to 3px
(4px was a bit too much space).
2022-03-24 11:07:37 -07:00
Lauryn Menard
b7747e51dd help_docs: Update various help docs for small changes.
- `user-groups`: Add warning for removing yourself.
- `restrict-wildcard-mentions`: Updated setting name to match UI.
- `format-your-message-with-markdown`: In-app help is now an icon.
- `web-public-streams`: Add link to Zulip's Rules of Use.
2022-03-24 10:59:03 -07:00
YashRE42
1b4ff0631c ui_init: Handle youtube play icon positioning via mouseenter handler.
Previously, we were experiencing a regression in the positioning of
the play icon for youtube previews, as such, this commit uses a
previously created `handle_video_preview_mouseenter` to ensure the
positioning is always correct.

This is an ugly way of doing things, because this could be handled
directly through CSS flexbox, however, it is an acceptable temporary
fix.
2022-03-24 10:56:21 -07:00
YashRE42
58184fc2a9 ui_init: Extract embedded video preview mouse handler.
This commit extracts the logic used to ensure that the play icon is
correctly positioned over the video preview for embedded videos, with
the intention that we can use this to fix a regression in the play
icon positioning for youtube video previews.
2022-03-24 10:56:21 -07:00
Aman Agrawal
d006b6cc3d message_list_view: For spectators, show login button for failed images.
We render a login button for images that failed to load for
spectators. The image failed to load most likely due to being
rate limited by the server.

Fixes #19840
2022-03-24 10:50:00 -07:00
Aman Agrawal
b799ec32b0 upload: Allow rate limited access to spectators for uploaded files.
We allow spectators access to uploaded files in web public streams
but rate limit the daily requests to 1000 per file by default.
2022-03-24 10:50:00 -07:00
Tim Abbott
abea1f4598 migrations: Add migration to fix Attachment cache.
This migration needs to be run after the previous commit is deployed
to a given Zulip installation, to fix any stale values of
is_realm_public and is_web_public.
2022-03-24 10:50:00 -07:00
Tim Abbott
d149af936d models: Rework Attachment.is_*_public to be a cache.
Previously, Attachment.is_realm_public and its cousin,
Attachment.is_web_public, were properties that began as False and
transitioned to True only when a message containing a link to the
attachment was sent to the appropriate class of stream, or such a link
was added as part of editing a message.

This pattern meant that neither field was updated in situations where
the access permissions for a message changed:

* Moving the message to a different stream.
* Changing the permissions for a stream containing links to the message.

This correctness issue has limited security impact, because uploaded
files are secured both by a random URL and by these access checks.

To fix this, we reformulate these fields as a cache, with code paths
that change the permissions affecting an attachment responsible for
setting these values to the `None` (uncached) state. We prefer setting
this `None` state over computing the correct permissions, because the
correct post-edit permissions are a function of all messages
containing the attachment, and we don't want to be responsible for
fetching all of those messages in the edit code paths.
2022-03-24 10:49:59 -07:00
Alex Vandiver
4f93b4b6e4 uploads: Skip the outgoing proxy if S3_KEY is unset.
When the credentials are provided by dint of being run on an EC2
instance with an assigned Role, we must be able to fetch the instance
metadata from IMDS -- which is precisely the type of internal-IP
request that Smokescreen denies.

While botocore supports a `proxies` argument to the `Config` object,
this is not actually respected when making the IMDS queries; only the
environment variables are read from.  See
https://github.com/boto/botocore/issues/2644

As such, implement S3_SKIP_PROXY by monkey-patching the
`botocore.utils.should_bypass_proxies` function, to allow requests to
IMDS to be made without Smokescreen impeding them.

Fixes #20715.
2022-03-24 10:21:35 -07:00
Tim Abbott
0d90bb2569 narrow: Fix messages being cached without flags set.
f0c680e9c0 introduced a call to
message_helper.process_new_message without first calling
message_store.set_message_flags on the message.

This resulted in it being possible as a race, when loading the Zulip
app to a stream/topic/near narrow, for a message to have the
`historical` flag be undefined due to not being initialized.

That invalid state, in turn, resulted in the message_list_view code
path for rendering the message feed incorrectly displaying additional
recipient bars around the message.

We could fix this by just calling message_store.set_message_booleans
in this code path. However, this bug exposes the fact that it's very
fragile to expect every code path to call that function before
message_helper.process_new_message.

So we instead fix this by moving message_store.set_message_booleans
inside message_helper.process_new_message.

One call point of concern in this change is maybe_add_narrow_messages,
which could theoretically reintroduce the double set_message_flags
bugs detailed in 9729b1a4ad. However, I
believe that to not be possible, because that call should never
experience a cache miss.

The other existing code paths were already calling
set_message_booleans immediately before
message_helper.process_new_message. They are still changing here, in
that we now do a cache lookup before attempting to call
set_message_booleans. Because the message booleans do not affect the
cache lookup and the local message object is discarded in case of a
cache hit, this should have no functional impact.

Because I found the existing comment at that call site confusing and
almost proposed removing it as pointless, extend the block comment to
explicitly mention that the purpose is refreshing our object.

Fixes #21503.
2022-03-24 08:10:14 -07:00
Tim Abbott
00332fd49d markdown: Simulate message flags in frontend markdown processor.
This eliminates an annoying bundle of complexity that caused the
frontend markdown processor's interface with the rest of Zulip's new
message processing code paths being more similar to that of a new
message from the server.

It also cuts down on code duplication.
2022-03-24 08:10:14 -07:00
Tim Abbott
2dd0b386fe echo: Clean up confusing handling of message flags in local echo.
The previous message.unread block in insert_local_message was
non-functional. markdown.apply_markdown is overriding what that set by
calling message_store.init_booleans, which happens to set the same
value for the `unread` flag, and then setting individual message
booleans as it finds elements like mentions during rendering.

Improve this situation partially by deleting the message_unread block,
and adding conversion logic to translate what the markdown processor
is doing into message flags.

Then, we can call message_store.set_message_flags just like we do when
processing new messages arriving via the API. This will be helpful
shortly, when we deduplicate the calls to
message_store.set_message_flags.

As noted in the lengthy TODO, this exposes the fact that we should
really rework how the frontend markdown processor returns the
mentioned state to its caller.
2022-03-24 08:10:14 -07:00
Lauryn Menard
7dd2ed85db help_docs: Update documentation about moving content.
Moves descriptions of notification and subsetting options
when moving content out of the instructions tab blocks
and into descriptive text.

Adds documentation for editing a topic via the message
recipient bar.

Also cleans up a few changes to UI interactions.

Co-authored-by: Alya Abbott <alya@zulip.com>
2022-03-23 23:46:22 -07:00
Tim Abbott
ec54b1e5a8 i18n: Update translation data from Transifex. 2022-03-23 23:41:11 -07:00
Abhishek Reddypalle
cab37b4aca tools: Add tool to count contributions across all major repositories.
This will allow Zulip release announcements to credit contributions
made to Zulip projects beyond the server in our release announcements.

Fixes #19044.
2022-03-23 14:20:30 -07:00
Greg Price
4befe4fc30 mailmap: Add a comment explaining this file.
In particular, link to upstream documentation.  This should help
contributors be able to see what it's doing and how to update it.

Copied straight from what we have in the zulip-mobile repo.
2022-03-23 14:12:43 -07:00
ditsuke
4d03a1b0b7 message_feed_view: Fit multiple images horizontally.
At the moment we fit only a single image per message per line. This is
wasteful of space as multiple images can be accomodated per line on
widescreen displays. This commit modifies the rendered_markdown
stylesheet to make this possible.

The comments detail various technical considerations.

Fixes #20975.
2022-03-22 22:35:17 -07:00
Alya Abbott
c121bec188 help center: Explain details of content moves. 2022-03-22 20:18:25 -07:00
Alya Abbott
6087f0daf1 help center: Improve organization of left sidebar. 2022-03-22 20:18:25 -07:00
Anders Kaseorg
24070c7ad8 beeminder: Accept float for payload["goal"]["pledge"].
The documentation at https://api.beeminder.com/#goal says this is
“number”; empirically, we do in fact get decimal points.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-22 21:08:48 -04:00
Aman Agrawal
6fcbe4091d web_public_streams: Change globe icon.
This revised globe icon avoids looking like a "language choice" icon
(as the previous one did), while still being recognizably Earth (and
not a disk with some things drawn on it) and not showing only North
America (a flaw with the Font Awesome 4.7 icon).

Used a derivative of icon from
https://unpkg.com/ionicons@5.5.2/dist/svg/earth.svg
with modified outline by Vlad Korobov.
2022-03-22 16:15:55 -07:00
Aman Agrawal
9a7fadbbeb zulip-icons: Move custom icons to shared folder. 2022-03-22 16:14:56 -07:00
Lauryn Menard
babe5ed44a help: Update notifications docs for changes.
Adds documentation for unread badge count to help article on
desktop notifications.

Generally, cleans up instructions and article structure for
the help article on pms, mentions and alerts.

Co-authored-by: Alya Abbott <alya@zulip.com>
2022-03-22 15:52:23 -07:00
Alex Vandiver
1ac0035f8c markdown: Allow whitespace overlaps in topic linkifiers.
`prepare_linkifier_pattern`, as of db934be064, adds a match to the
end of the regex, of either the end of string, or a non-word character
-- this is in place of a negative look-ahead, which is no longer
possible in re2.  This causes the regex to consume trailing
whitespace, and thus not be able to match twice in succession with
`pattern.finditer` -- "#1234 #5678" fails to match because the space
is consumed by the first match of the regex.

Rather than use `pattern.finditer`, write own own version, which
rewinds over the non-word character consumed after the match, if any.
This allows the same "after" non-word character to also satisfy the
"before" of the next match.

Fixes #21502.
2022-03-22 15:40:03 -07:00
Sahil Batra
d89b5042a9 settings: Send config_data to bot update API only if required.
There is config_data for the embedded bots only for giphy and
followup bots, so we send "config_data" field to API only for
those bots and not others. Send config_data field as {} to
the API for other bots raised InternalError earlier.
2022-03-22 15:38:43 -07:00
Sahil Batra
630abf57d9 settings: Remove bot from "Active bots" list on changing owners.
On changing bot owner, "delete" event is sent to the previous
owner if previous owner is not an admin. We were ignoring the
"delete" event in webapp previously, but now we update the
bots page in personal settings to delete the bot. Note that
we do not remove the bot from the organization list of bots
currently, since list_widget does not support removing a
row as of now.

In case of previous owner being an admin, the previous owner
receives "update" event and thus the bots list is updated
from that event.

The code for ignoring "delete" event was added in fba2708bbc,
to basically avoid failed lookup for id in the organization
list of bots. I have tested and there cannot be a case of
a failed lookup in the list as per current code for list_widget
module and we are anyways safe after a reload or after closing the
overlay as the list will be updated correctly.

Discussion thread -
https://chat.zulip.org/#narrow/stream/321-settings-system/topic/List.20render.20.2315033

Fixes #20856.
2022-03-22 15:38:43 -07:00
Alex Vandiver
b9e428dd5d lightbox: Make the "download" link use the new download endpoint.
The "download" attribute on the button only functions for same-origin
requests; thus, the download endpoint must be used in order for the
"Download" button to function for uploaded images which are stored in
S3, and thus served from a different origin.

This is only done for uploaded images; it does not address a similar
problem with Camo, when Camo is hosted on a different hostname.

Fixes: #19238.
2022-03-22 15:05:02 -07:00
Alex Vandiver
abed174b12 uploads: Add an endpoint which forces a download.
This is most useful for images hosted in S3, which are otherwise
always displayed in the browser.
2022-03-22 15:05:02 -07:00
Alex Vandiver
d7b59c86ce puppet: Build wal-g from source for aarch64.
Since wal-g does not provide binaries for aarch64, build them from
source.  While building them from source for arm64 would better ensure
that build process is tested, the build process takes 7min and 700M of
temp files, which is an unacceptable cost; we thus only build on
aarch64.

Since the wal-g build process uses submodules, which are not in the
Github export, we clone the full wal-g repository.  Because the
repository is relatively small, we clone it anew on each new version,
rather than attempt to manage the remotes.

Fixes #21070.
2022-03-22 15:02:35 -07:00
Alex Vandiver
4d4c320a07 puppet: Switch from ntp to chrony.
Chrony is the recommended time server for Ubuntu since 18.04[1], and
is the default on Redhat; it is more accurate, and has lower-memory
usage, than ntp, which is only getting best-effort security
maintenance.

See:
- https://wiki.ubuntu.com/BionicBeaver/ReleaseNotes#Chrony
- https://chrony.tuxfamily.org/comparison.html
- https://engineering.fb.com/2020/03/18/production-engineering/ntp-service/
2022-03-22 13:07:27 -07:00
Lauryn Menard
b67288db67 help_docs: Extend options for getting links to Zulip content.
Extends the linking to Zulip documentation to cover:

- Getting URLs to messages via the message timestamp.
- Getting links to topics via the three-dots menu.
- Getting links to streams via right-click context menu.

Creates a new tabbed section for using the browser
address bar to copy URLs.
2022-03-22 12:38:09 -07:00
Lauryn Menard
453cb409cf help_docs: Update button description for archive a stream doc.
In the current UI, the button for archiving a stream is an icon
while previously it was text ('Archive'). Updates documentation
to refer to it as so.
2022-03-22 12:08:14 -07:00
Lauryn Menard
ba7695e5de help_docs: Revise mute-a-topic help doc.
Generally, revises a number of out of date information in this
help article.
2022-03-22 12:06:24 -07:00
Tim Abbott
2f929bee2f compose: Match width of topic compose to left sidebar space.
Previously, the maximum width for the topic input in the compose box
was artificially limited to 20% of the width of the compose box.

While this may have had some useful role in encouraging short topics,
we can teach that idea in other ways, and it seems more helpful to
have the input length match what works well for viewing topics in the
left sidebar without being cut off.
2022-03-22 11:52:26 -07:00
yogesh sirsat
8d40199729 settings_users: Display error inside deactivate user modal.
Currently, when deactivating a user, we have a "Saving..." loading
indicator and any error message displayed in the heading area of the
users table.

Migrate this to instead do the loading indicator and error message
inside the modal, where it's more in context.
2022-03-22 11:14:48 -07:00
Tim Abbott
ceb9dd5854 migrations: Fix confusing output for migration 0383.
* Don't print the empty list for the vast majority of realms where
  this is a noop.

* Make output a little more clear that this isn't revoking all
  Confirmations, just those associated with deactivated users.
2022-03-21 23:25:06 -07:00
Rishabh-792
3f0b0ee88c settings: Improve help icons in settings.
Improved the contrast of ? and i icons by changing their opacity to a
consistent 0.6, going to 1 on hover.

Changed the colour of playground icon by testing and added spacing
between title and the playground icon by changing the icon margin.

With some TODO comments added by tabbott for readability.

Fixes part of #20484.
2022-03-21 18:07:16 -07:00
SantamRC
373f700736 widgets: Fix line wrapping of todo descriptions.
The simplest solution for doing this involves adding some divs.

Fixes #20523.
2022-03-21 17:52:45 -07:00
My-Name-Is-Nabil
cc32a3afff compose: Fix alignment of close buttons in stream invite banners.
Close buttons are misaligned if the warning banner text takes up two lines.

We increase the specificity of the selectors to ensure that this CSS
overrides Bootstrap.

Fixes #20839.
2022-03-21 17:47:12 -07:00
Tim Abbott
7bc0e70693 recent topics: Revert time format changes for now.
As detailed in this conversation:

https://chat.zulip.org/#narrow/stream/137-feedback/topic/recent.20topics.20timestamps/near/1337670

This time format change is not working out as an improvement for at
least some users, myself included.

I think we do want to use some of the refinements attempted here (and
in particular, I'm keeping the new function with its nice test suite),
but I think it's better to revert now and fix forward in a future
release.

See #19775 for added background.
2022-03-21 17:37:32 -07:00
jai2201
040363f6c7 tippy: Render tooltip from tippy.js for stream_sorter_toggle.
Render tooltip to stream_sorter_toggle buttons using
appendTo method of tippy.js to ensure that tooltip doesn't
get hidden behind the parent container and is visible
completely.

Fixes #21329.
2022-03-21 17:35:04 -07:00
somesh202
cf5a70a958 settings: Fix buggy heading hover behavior for some settings panels.
Previously, hovering over the table headers in the code playground
table in the dark theme looked wrong. We were able to trace this issue
to the table-sticky-headers class not having been applied to this
table. The `alert-words` table was also affected and is fixed as well.

This also adds the `actions` class in alert-words table to fix the
inconsistency of the actions column in that table.

We've done an audit and these are the only two instances of either
bug in the HTML templates for a settings table.
2022-03-21 17:24:23 -07:00
Biki-das
64ec62b15c portico: Fixed responsive of integration request buttons.
Previously, these buttons looked broken in mobile size screens.

Fixes #20799.
2022-03-21 16:44:06 -07:00
Aman Agrawal
61cf9d1843 compose: Minor realignment of compose icons. 2022-03-21 16:41:18 -07:00
Alex Vandiver
e2f4b284db docs: Remove teleport from example list of services.
This is not expected on generic Zulip servers.
2022-03-21 16:33:28 -07:00
Alex Vandiver
f39ee5a16c docs: Remove references to supervisorctl (re)start all. 2022-03-21 16:33:28 -07:00
Alex Vandiver
a4d0f03319 scripts: Switch to stop-server/restart-server.
stop-server and restart-server address all services which talk to the
database, and are thus more correct than restarting or stopping
everything in supervisor.

This is possible now that the previous commit ensures that the zulip
user can read the zulip installation directory during
`create-database`; previously, that directory was still owned by root
when `create-database` was run, whereas now it is in
`~zulip/deployments/`.
2022-03-21 16:33:28 -07:00
Alex Vandiver
c0cc98c6a8 install: Re-order final steps.
Move database creation to immediately before database initialization;
this means it happens in a directory readable by the `zulip` user, as
well as placing it alongside similar operations.  It removes the check
for the `zulip::postgresql_common` Puppet class; instead it keeps the
check for `--no-init-db`, and switches to require
`zulip::app_frontend_base`.  This is a behavior change for any install
of `zulip::postgresql_common`-only classes, but that is not a common
form -- and such installs likely already pass `--no-init-db` because
they are warm spare replicas.

As a result, all non-`zulip::app_frontend_base` installs now skip
database initialization, even without `--no-init-db`.  This is clearly
correct for, e.g. Redis-only hosts, and makes clearer that the
frontend, not the database host, is responsible for database
initialization.
2022-03-21 16:33:28 -07:00
Alex Vandiver
394f1eadde setup: Rename postgresql-init-db to create-database.
The old name was confusingly similar to initialize-database.
2022-03-21 16:33:28 -07:00
Alex Vandiver
086c0328bd docs: Be explicit about how to skip database creation. 2022-03-21 16:33:28 -07:00
Lauryn Menard
b70b925716 help_docs: Update reading-strategies for changed icon.
Removes reference to icon (previously a magnifying glass) in
text for filtering streams and replaces it with direction
for clicking on the STREAMS header in the left sidebar.
2022-03-21 16:14:07 -07:00
Lauryn Menard
ca395227b2 help_docs: Update mute-a-stream for menu option text.
Corrects text in instructions for the menu option when
muting and unmuting streams.
2022-03-21 16:12:58 -07:00
Lauryn Menard
ddab1d9b07 help_docs: Update mobile-notifications to use 'toggle'.
We'd like to use 'toggle' for consistency when referring to
check boxes in the help center documentation.
2022-03-21 16:12:20 -07:00
Lauryn Menard
210268f264 help_docs: Update manage-inactive-streams for settings header.
Correct out of date section header in display settings.
2022-03-21 16:12:20 -07:00
Lauryn Menard
1b1c479333 help_docs: Update invite-new-users for dropdown option.
Corrects an out of date dropdown option in organizational
settings for enabling email sign-up.
2022-03-21 16:12:20 -07:00
Lauryn Menard
517b2a5e10 help_docs: Update email-notifications for setting header.
Updates out of date reference to a Notifications setting header,
and also updates instructions for a check box to use 'toggle'.
2022-03-21 16:12:19 -07:00
Lauryn Menard
4f735aeb0e help_docs: Update configure-default-view to use 'toggle'.
We'd like to use 'toggle' for consistency when referring to
check boxes in help center documentation instructions.
2022-03-21 16:11:42 -07:00
Alex Vandiver
a2c8be9cd5 puppet: Increase download timeout from 5m to 10m.
The default timeout for `exec` commands in Puppet is 5 minutes[1].  On
slow connections, this may not be sufficient to download larger
downloads, such as the ~135MB golang tarball.

Increase the timeout to 10 minutes; this is a minimum download speed
of is ~225kB/s.

Fixes #21449.

[1]: https://puppet.com/docs/puppet/5.5/types/exec.html#exec-attribute-timeout
2022-03-21 15:47:04 -07:00
Austin Riba
0943b38300 lightbox: Add tippy tooltip to lightbox title.
This commit adds a tippy tooltip to the lightbox title which enables the
user to view the filename of an image if the filename is different than
the image title.

Fixes: #21333
2022-03-21 15:36:10 -07:00
Austin Riba
3df0cacd9e messages: Show tippy tooltip for inline images.
This commit adds a tippy tooltip for inline image previews in messages.

There exists some (reasonable) logic in `static/js/util.js` which
overrides all title attributes for links to user-uploaded content to
ensure they always display "Download <filename>". This doesn't make
sense for inline images specifically because they will be opened in a
ligthbox, so we prevent that.

There is an additional tippy instance created in `static/js/tippyjs.js`
to add tippy tooltips to inline images, which takes advantage of the now
preserved title attribute of the parent link.
2022-03-21 15:36:10 -07:00
Lauryn Menard
6063d063b7 narrow: Add message edit history check for near links.
If realm has edit history enabled, adds a check of message
edit history before rewriting operators.
2022-03-21 13:32:11 -07:00
Lauryn Menard
0844a80d66 edit_history: Check edit history for stream and topic match.
Creates a helper function in `message_edit.js` that loops over
a message's edit history to see if a stream and topic pair
existed at some point in the message history.

Exports `util.lower_same` function to use for comparing
edit history topics as lowercase.

Also adds test for new function in `mesage_edit` node tests.
2022-03-21 13:32:11 -07:00
Aman Agrawal
f0c680e9c0 narrow: narrow: Make near links work on topic / stream change.
We check if stream and topic present in the URL match that of
the message in its current state. If message is not available locally,
we fetch it before rendering the narrow.

Fixes #15290.
2022-03-21 13:32:11 -07:00
Tim Abbott
86dacee91e narrow: Reorder early startup logic in narrow.activate.
This is important preparation for a recursive call we will be adding
to the function.
2022-03-21 13:32:11 -07:00
Ganesh Pawar
b7edd5df2f settings: Place the saving indicator in the header.
Fixes #21441.
2022-03-21 13:30:38 -07:00
Aman Agrawal
7a25a80242 message_edit: Move muted status of the topic when moved between streams.
Add support for moving MutedTopic entries to another stream where
the user has access to shared history in both streams and
`propagate_mode != "change_one"`.

Also, we delete them the current user does not have access to the
target stream.
2022-03-21 12:42:39 -07:00
Lauryn Menard
1b3e003b53 help_docs: Update stream-sending-policy help doc.
Uses new `select-stream-view-general.md` for instructions.

Also, clarifies location of button/icon as well as the
header for the privacy setting in the update modal.

Finally, updates save instruction to use `save-changes.md`.
2022-03-21 12:26:03 -07:00
Lauryn Menard
7c60ff384d help_docs: Update change-the-privacy-of-a-stream help doc.
Uses new `select-stream-view-general.md` for instructions.

Also, updates description to include web-public streams with link
to documentation about them, and clarifies location of button/icon
as well as the header for the privacy setting in the update modal.

Finally, updates save instruction to use `save-changes.md` and
renumbers instructions list to only use '1'.
2022-03-21 12:26:03 -07:00
Lauryn Menard
27d669ab62 help_docs: Update stream-notifications help doc.
Uses new `select-stream-view-personal` for instructions.

Also, moves one sentence notes to be under header vs tab block,
and updates numbers used in instruction list to all be '1',
and clarifies text about notifications table in general personal
setting.
2022-03-21 12:26:03 -07:00
Lauryn Menard
78af84f6a1 help_docs: Update message-retention-policy help doc.
Uses new `select-stream-view-general.md` for instructions.

Also, updates the text for locating the button/icon.
2022-03-21 12:26:03 -07:00
Lauryn Menard
3989bfe1ff help_docs: Update rename-a-stream help doc.
Uses new `select-stream-view-general.md` for instructions.

Also, updates text for locating the button/icon and uses
the `save-changes.md` to correct the final instruction/step.
2022-03-21 12:26:03 -07:00
Lauryn Menard
b925c2f3de help_docs: Update message-a-stream-by-email help doc.
Uses new `select-stream-view-general.md` for instructions.
2022-03-21 12:26:03 -07:00
Lauryn Menard
7d2abdf56a help_doc: Update change-the-color-of-a-stream help doc.
Uses new `select-stream-view-personal.md` for instructions.

Also, clarifies location of setting (header).
2022-03-21 12:26:03 -07:00
Lauryn Menard
f0409517b3 help_docs: Update pin-a-stream help doc.
Uses new `select-stream-view-personal.md` for instructions.

Also, clarifies where the checkbox is located (header) and
updates instruction to use 'toggle' vs 'click' for checkbox.
2022-03-21 12:26:03 -07:00
Lauryn Menard
2e69b66fec help_docs: Update change-the-stream-description help doc.
Uses new `select-stream-view-general.md` in instructions.

Also, moves admin warning to top of the article and updates
description text for clarity (instead of 'apps', specify
which apps have the description in the stream view).
2022-03-21 12:26:03 -07:00
Lauryn Menard
f46b231c0c help_docs: Update add-or-remove-users-from-a-stream help doc.
Uses new `select-stream-view-subscribers.md` in instructions.

Also, adds a tip to bulk add users to a stream.
2022-03-21 12:26:03 -07:00
Lauryn Menard
9f843b924a help_docs: Add markdown helpers for stream view tabs.
Adds three markdown documents in to be used for help center
articles that have instructions that refer to actions in
the 3 new tabs in the 'Subscribed/All streams' view:

- General: stream name, description and permissions changes.
- Personal: personal stream settings, such as color, changes.
- Subscribers: adding or removing users from streams.
2022-03-21 12:26:03 -07:00
Tim Abbott
4da2f154e0 create_user: Improve --help output.
This includes documenting this as not the primary way to
programatically create users in Zulip.
2022-03-21 12:05:59 -07:00
Tim Abbott
aebd81c440 management: Remove unnecessary default=SUPPRESS logic. 2022-03-21 12:05:59 -07:00
Tim Abbott
2328a81f55 devtools: Remove development-only user creation management commands.
We now have nicer version available not only to developers, and it's
definitely better to deduplicate these.
2022-03-21 12:05:59 -07:00
Tim Abbott
e16043547b management: Add new create_realm management command.
This is intended for rare situations where one is creating multiple
realms via a script.

After all the preparatory refactoring in this last several commits, we
can now provide a working implementation of a create_realm management
command.
2022-03-21 12:05:59 -07:00
Tim Abbott
2be2393d3e create_user: Extract get_create_user_params.
We set nocoverage for the new function. Ideally it'd eventually get an
automated test, but we don't want to block this helpful refactoring on
doing so.
2022-03-21 12:05:59 -07:00
Tim Abbott
be6ab93b37 create_user: Rename pw => password for readability. 2022-03-21 12:05:59 -07:00
Tim Abbott
2bc1cd6ab4 create_user: Fix overly large try/except block.
Only the do_create_user call can throw IntegrityError, and it's a lot
more readable to thus scope the try/except where it belongs.
2022-03-21 12:05:59 -07:00
Tim Abbott
ed3569a470 create_user: Simplify parameter processing.
We remove a bit of error handling for cases where someone provided
only one of the email and full name parameters, with the benefit of
this being a lot cleaner.
2022-03-21 12:05:59 -07:00
Tim Abbott
279b99ab23 create_user: Fix unnecessary nesting of input logic. 2022-03-21 12:05:59 -07:00
Tim Abbott
57fa62ae4b initial_password: Add explicit development environment assertion.
The construction of INITIAL_PASSWORD_SALT is such that it should only
be set in development environments, but we should enforce this rule.
2022-03-21 12:05:59 -07:00
Tim Abbott
5393ce11c7 management: Clean up create_user password logic.
* Assert that we're in a development environment when appropriate.
* Add useful logging messages, including print_initial_password details.
2022-03-21 12:05:59 -07:00
Tim Abbott
6b00c748fd onboarding: Deduplicate realm creation initial user logic.
We now call this function inside do_create_user(...,
realm_creation=True), which generally improves readability and
robustness of the codebase.

This fixes a bug where this onboarding content was not correctly done
when creating a realm via LDAP, and also will be important as we add
new code paths that might let you create a realm.
2022-03-21 12:05:59 -07:00
Tim Abbott
a117b224a7 onboarding: Refactor setup_realm_internal_bots call.
This improves robustness of any code paths calling do_create_realm,
which previously needed to call this correctly to achieve the same
results as creating a user via the UI.

This also fixes a bug where this code was not called if a realm were
created using the LDAP code path.
2022-03-21 12:05:59 -07:00
Tim Abbott
b4507df8fa create_user: Remove tos parameter from management command.
This parameter was introduced in
ea11ce4ae6, and no longer serves a
purpose. Zulip will already correctly record that the user has not
agreed to ToS, and either prompt them on first login or not depending
whether the server is configured to require ToS.
2022-03-21 12:05:59 -07:00
Tim Abbott
fddd83394e create_user: Specify tos_version=None explicitly in automation.
This is an important design detail, so we document this aspect of
creating users via both the management command and API code paths with
an explicit parameter value and comment.
2022-03-21 12:05:59 -07:00
Tim Abbott
4f3894f9f1 management: Improve help text for create_user. 2022-03-21 12:05:59 -07:00
Tim Abbott
9761711351 management: Extract add_create_user_args.
This will avoid code duplication when adding a create_realm management command.
2022-03-21 12:05:59 -07:00
Mateusz Mandera
af5d0d6f5e bots: Don't allow admins to change owner of bot with can_create_users.
Ordinary organization administrators shouldn't be allowed to change
ownership of a bot with the can_create_users permission.

This is a special permission that is granted manually by server
administrators to an organization (to a UserProfile of the org owners'
choice) after approval by a server administator. The code comments
provide more detail about why this is sensitive.
2022-03-20 17:18:21 -07:00
Lorenzo Milesi
88e0d1b111 docs: Add more details on configuring LDAP group restriction.
Fixes #338.

Co-authored-by: Mateusz Mandera <mateusz.mandera@zulip.com>
2022-03-20 17:04:16 -07:00
Alex Vandiver
9e850b08f3 puppet: Fix the PostgreSQL paths to recovery.conf / standby.conf. 2022-03-20 16:16:04 -07:00
yogesh sirsat
3be023911b recent topics: Close Profile popover while muting user.
Fixes: #21456
2022-03-20 16:14:29 -07:00
Alex Vandiver
1bd5723cd2 puppet: Add a prometheus monitor for tornado processes. 2022-03-20 16:12:11 -07:00
Alex Vandiver
6b91652d9a puppet: Open the grok_exporter port.
The complete grok_exporter configuration is not ready to be committed,
but this at least prepares the way for it.
2022-03-20 16:12:11 -07:00
Alex Vandiver
6558655fc6 puppet: Add rabbitmq prometheus plugin, and open the firewall. 2022-03-20 16:12:11 -07:00
Alex Vandiver
bdd2f35d05 puppet: Switch czo to using zulip_ops::app_frontend_monitoring.
This was clearly intended in f61ac4a28d
but never executed.
2022-03-20 16:12:11 -07:00
Alex Vandiver
17699bea44 puppet: postgresql_backups is auto-included if s3_backups_bucket is set.
Since 6496d43148.
2022-03-20 16:12:11 -07:00
Alex Vandiver
bedc7c2986 puppet: Smokescreen is now auto-included in standalone.
Since c33562f0a8.
2022-03-20 16:12:11 -07:00
Simmo Saan
307a7d8104 bitbucket3: Fix docs typo Zuilp -> Zulip 2022-03-20 16:10:03 -07:00
Felix
780bda0d40 integrations: Fix BigBlueButton password length.
The BigBlueButton integration had a problem with generating
the random password with only 12 characters. This would
cause the attendeePW to be the same as the moderatorPW,
which might be fine but seems like something that could be an
error in a future version of BigBlueButton.
2022-03-20 16:09:36 -07:00
YashRE42
33af1c1cd6 resize: Fix expensive navbar.resize_app calls leaving recent_topics.
It turns out that the bug this call hopes to fix only happens when the
user first loads the page to recent_topics and then navigates to a
view with a message list (any other view), but we'd make this call
every time the recent topics table was hidden.

Hence, this commit makes it such that we only make that call if (1)
the page is loaded to recent_topics and (2) we're switching from
recent_topics to a message list view for the first time.  We achieve
(1) via binding a handler via ui_init.initialize_everything and (2) by
binding the handler as `.one`, so that it's unbound after its first
invocation.

Additionally, we use window.requestAnimationFrame to prevent this
forcing the browser to do a reflow unnecessarily.

Combined with other commits in this series, this fixes a major
performance problem when leaving recent topics for another view.

See #20255 for details.
2022-03-18 18:23:34 -07:00
Tim Abbott
83d02af9aa i18n: Update translation data from Transifex. 2022-03-18 17:52:00 -07:00
strifel
a967a86b10 integration: Generate dynamic name for BigBlueButton video calls.
The name for a BigBlueButton meeting is now generated from the stream
name and topic name.

The createTime option is used to have the user redirected to a link
that is only valid for this meeting.

Even if the same link in Zulip is used again, a new createTime
parameter will be created, as the Meeting on the BigBlueButton server
has to be recreated.

Fixes #16498.
Fixes #20509.
Fixes #20804.
2022-03-18 17:27:39 -07:00
YashRE42
0d117ab033 narrow: Don't try to save the recent_topics pre_narrow_offset.
The previous logic didn't make sense -- the scroll offset in recent
topics is not a scroll offset within a message list, so saving it was
useless at best.

However, it was actually much worse than that, because trying to save
the pre-narrow offset while in the process of navigating away from
recent topics had the side effect of forcing a reflow, which resulted
in very expensive browser rendering to no purpose.

Adding this commit to the rest of the series of commits fixing
rendering issues when leaving recent topics, this commit results in an
impressive 3.05 s decrease in the first renarrow, an ~ 300 ms decrease
in the second renarrow and an ~ 500 ms decrease in the third renarrow.

There are still further forced reflows which could be reduced in this
render path, but they seem to not be as severe.
2022-03-18 17:15:36 -07:00
YashRE42
c4bb181056 recent_topics: Save offset when navigating from narrow or "all".
This is partially a prep commit to correctly saving/restoring the
position of the blue message select box when using browser
back/forward navigation, and partially a bug fix that ensures that
switching from "all_messages" to "recent_topics" preserves one's
position in "all_messages".
Note that this is with regards to saving the visual position of the
selected message, not about saving "which message was selected".
2022-03-18 17:07:53 -07:00
Tim Abbott
9d460a513e tools: Fix typo in previous commit. 2022-03-18 16:57:51 -07:00
Tim Abbott
a9d7a15ce2 tools: Add missing exclude to check-capitalization.
This tool needs improvement, but this fixes main failing CI.
2022-03-18 16:41:48 -07:00
Nikhil Maske
5e8c8bfc0f settings_config: Rename "Unread count summary" to "Unread count badge". 2022-03-18 15:29:53 -07:00
rht
6be44a6971 docs: mobile-push-notifications.md: Fix grammar problems found by LanguageTool. 2022-03-18 15:28:16 -07:00
rht
74780d24d5 docs: management-commands.md: Fix grammar problems found by LanguageTool. 2022-03-18 15:28:16 -07:00
rht
507851c25a docs: requirements.md: Fix grammar problems found by LanguageTool. 2022-03-18 15:28:16 -07:00
yogesh sirsat
0d31781aed popovers: Support reactivating users from user profile popover.
If the target user is deactivated, `Reactivate this user` will be
shown as one of the options in the small user profile popover, where
`Manage this user` would usually be.

We rename `show_manage_user_option` to `can_manage_user` because now
it will also be used as the common condition for whether the current
user has administrative permission to active or deactivate the target
user.

The implementation closely follows the existing deactivation modal.

Fixes #21428.

chat.zulip.org discussion:
design > reactivate user from user popover
2022-03-18 15:19:13 -07:00
yogesh sirsat
1fb4dd2eee popovers: Remove parens around "This user has been deactivated".
These feel like unnecessary clutter, given that we also have italic
styling for this element.

Fixes part of #21428.
2022-03-18 15:19:13 -07:00
yogesh sirsat
211f137fc8 popovers: Remove the "Manage this user" link for deactivated users.
The "Manage this user" link did not work correctly for deactivated
users.

Fixes part of #21428.
2022-03-18 15:18:56 -07:00
N-Shar-ma
ecf557eab9 typeahead: Update / hide typeahead menu on clicking outside.
When a user clicks outside the typeahead menu, inside the typing area,
the cursor position potentially changes, so `lookup` is called, which
considers the new cursor position and accordingly hides, continues
showing, or updates the typeahead menu.

This fixes the bug where even after clicking elsewhere, the old
typeahead menu continued showing and on making a selection, the text
was inserted at the wrong (new) position.

Fixes: #21302.
2022-03-18 14:59:28 -07:00
yogesh sirsat
84cdf03bb2 settings_users: Help center link in deactivate confirmation modal. 2022-03-18 14:54:01 -07:00
sayamsamal
5fb3aa4f44 CSS: Deduplicate CSS in image_upload_widget.css. 2022-03-18 14:43:13 -07:00
sayamsamal
b06178767b settings: Fix upload spinner alignment in image upload widgets.
The upload spinners for all of the image upload widgets were in
wrong alignment due to the use of magic numbers to center them.

This commit replaces the above mentioned approach with the use
of flexbox to fix alignment issues across all the image upload widgets.
2022-03-18 14:43:13 -07:00
sayamsamal
7fcac3288b settings: Fix alignment issues for non-English languages.
Due to differences in length of the words for different languages
there were alignment issues in the organization profile settings.

This commit uses flexbox to ensure that the alignment stays correct
for any changes in language/word length.

Fixes #21385
2022-03-18 14:43:13 -07:00
Lauryn Menard
d3dca4f7de help_docs: Update instructions for change stream color.
Updates instructions for changing a stream's color so that
there are no missing or incorrect steps.
2022-03-18 14:28:20 -07:00
Tim Abbott
f8146cfaa1 help: Fix label for Subscribed tab in stream settings. 2022-03-18 14:28:20 -07:00
Tim Abbott
e45cebd636 message_edit: Fix unmute of topic when topic name is edited.
Previously, when a topic was edited (including being resolved), it
would become unmuted for any users who had muted it, which was
annoying.

While it's not possible to determine the user's intent completely,
this is clearly incorrect behavior in the `change_all` case, such as
resolving a topic.

The comments discuss some scenarios where we might want to enhance
this further, but this is the best we can do without large increases
in complexity.

Fixes #15210.

Co-authored-by: akshatdalton <akshat.dak@students.iiit.ac.in>
2022-03-18 12:32:46 -07:00
byshen-dev
26d97ce7e3 migrations: Change realm field to be not null in Attachment.
he possibility for it being null was likely an oversight -- it should
have been removed after the early migrations to backfill the field
when it was added.

We've confirmed there are no existing violations of this invariant in
Zulip Cloud.
2022-03-18 12:01:15 -07:00
Steve Howell
29e4342738 puppeteer tests: Split stream tests to two files.
This is just moving code around.
2022-03-18 12:20:48 -04:00
Steve Howell
db5f39c506 puppeteer tests: Try to diagnose/fix unsubscribe flakes.
Doing these in a loop may help us figure out whether the
flakes are somehow related to the initial conditions when
we run the test vs. some race that can happen later in the
loop.

I add the console statements mostly to facilitate debugging,
but they appear to actually reduce the problem, as the code
comments indicate.
2022-03-18 12:20:48 -04:00
Steve Howell
a5ec78c6ab puppeteer tests: Avoid stream creation flake.
We have a flake related to verifying that the app
prevents us from creating stream with duplicate names,
and my hypothesis is that it has to do with us not
waiting for the stream creation UI to fully appear. This flake
is probably a consequence of us recently making the stream
creation UI more like the stream editing UI, and thus
waiting for Desdemona to appear was giving us false
confidence that the page actually loaded.

I could be completely wrong about this solving the
flake, but the code change here is sensible regardless.
2022-03-18 12:20:48 -04:00
Lauryn Menard
1168258285 help_docs: Update how to access drafts in help center doc.
Small update of how to access drafts due to the removal of the
button at the bottom of the page. Drafts are now accessed through
the left sidebar.
2022-03-17 17:25:28 -07:00
Anders Kaseorg
b0217d0ec6 password_quality: Switch zxcvbn to zxcvbn-ts.
zxcvbn has had no releases since 2017.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 15:24:46 -07:00
Anders Kaseorg
7aa03e9d2a dependencies: Upgrade JavaScript dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 15:24:46 -07:00
Anders Kaseorg
7d4b02738d install-node: Upgrade Node.js from 16.14.0 to 16.14.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 15:24:46 -07:00
Anders Kaseorg
e2e645a183 browserslist: Stop transpiling for pre-ES2019 browsers.
Specifically, this desupports:

android 4.4.3-4.4.4
baidu 7.12
ie 11
kaios 2.5
op_mini all

although we’ve already been blocking IE 11 since 3.0 (#14662).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 15:24:46 -07:00
Mateusz Mandera
995cbc69b4 migrations: Add migration to revoke invites from old deactivated users.
This is a natural follow-up to
93e8740218 - invitations sent by users
deactivated before the commit still need to be revoked, via a
migration.

The logic for finding the Confirmations to deactivated is based on
get_valid_invite_confirmations_generated_by_user in actions.py.
2022-03-17 15:16:05 -07:00
Anders Kaseorg
6aec27e646 js: Fix no-jquery/no-parse-html-literal.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:16:34 -07:00
Anders Kaseorg
e32ec3f7eb ui_report: Fix inappropriate $ prefix.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:16:34 -07:00
Anders Kaseorg
215791db1e eslint: Enable no-jquery/deprecated rules.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:10:23 -07:00
Anders Kaseorg
eb7770565a js: Fix no-jquery/no-sizzle.
The :first and :last selectors were deprecated in jQuery 3.4.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:10:23 -07:00
Anders Kaseorg
8ef2c0a604 js: Fix no-jquery/no-ready-shorthand.
The ready method was deprecated in jQuery 3.0, because its behavior
has nothing to do with the selector; it always waits for the page to
become ready, not a specific element.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:10:23 -07:00
Anders Kaseorg
da0658967c js: Fix no-jquery/no-event-shorthand.
The event shorthand methods were deprecated in jQuery 3.3.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:10:23 -07:00
Anders Kaseorg
5d77381667 node_tests: Add a few missing $ prefixes.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:10:23 -07:00
Anders Kaseorg
44844743cd settings_account: Fix password quality meter.
It was broken by commit f5fbf5f0e0
“change_password: Migrate modal to dialog_widget” (#20193), because
the new_password input didn’t exist when we tried to install an event
handler for it.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:08:20 -07:00
Anders Kaseorg
a3d89e049a models: Add missing type annotations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:07:09 -07:00
Anders Kaseorg
62e049b25a models: Type nullable message_content_delete_limit_seconds as Optional.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-17 13:07:09 -07:00
Alex Vandiver
c35a783c35 docs: Minor wording fixes to warm standby replication docs. 2022-03-17 12:53:26 -07:00
Alex Vandiver
2c26ad3714 docs: Break out and clarify wal-g backup configuration. 2022-03-17 12:53:26 -07:00
Alex Vandiver
6489c832a3 puppet: Upgrade third-party package versions. 2022-03-17 11:44:05 -07:00
Alex Vandiver
6b0876063f bootstrap-awscli: Upgrade awscli version. 2022-03-17 11:44:05 -07:00
Steve Howell
bffd73fe44 node tests: Add spectator test for search suggestions.
This is a prep commit to help us soon remove 44 months worth of
bit-rotted code.
2022-03-17 12:07:53 -04:00
Steve Howell
599a70d6f4 node tests: Add test for topic_suggestions.
This is a prep commit to help us soon remove 44 months worth of
bit-rotted code.
2022-03-17 12:07:53 -04:00
Steve Howell
097852bb06 node tests: Add coverage for clear_search_form.
This is a prep commit to help us soon remove 44 months worth of
bit-rotted code.
2022-03-17 12:07:53 -04:00
Steve Howell
a9f83a5805 node tests: Add test for people suggestions.
This is a prep commit to help us soon remove 44 months worth of
bit-rotted code.
2022-03-17 12:07:53 -04:00
Steve Howell
d162bd17a9 node tests: Use now/future naming scheme for search tests. 2022-03-17 12:07:53 -04:00
Anders Kaseorg
4b712b49ef eslint: Enable eslint-plugin-no-jquery.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-16 12:52:07 -07:00
Anders Kaseorg
f84a2c08d5 js: Prefix jQuery object variable names with $.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-16 12:52:07 -07:00
Anders Kaseorg
f21842e920 requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-16 10:43:23 -07:00
Steve Howell
dfab993e7d node tests: Avoid narrow_state mocking in pm_list* tests.
We now use narrow_state directly in pm_list and pm_list_data
tests, rather than mocking it with our `override*` helpers.

In some places I use an actual Filter() object, but
in places where the only testing concern is that the
active is narrow, I use a stub value.

We will continue to mock narrow_state in most places.
In addition to avoiding test-setup complications, we want
to avoid incidental line coverage on narrow_state that
only indirectly validates its behavior. Part of the
trickiness in avoiding narrow_state mocking is that
you often would have to introduce "real" Filter objects,
and the API for Filter objects is somewhat less than
ideal, and its wordiness can distract from the main
point of the tests.

Hopefully the changes here reflect the correct tradeoffs.
2022-03-16 11:57:58 -04:00
524 changed files with 24913 additions and 20672 deletions

View File

@@ -2,4 +2,4 @@
> 0.15% in US
last 2 versions
Firefox ESR
not dead
not dead and supports async-functions

View File

@@ -7,6 +7,8 @@
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:no-jquery/recommended",
"plugin:no-jquery/deprecated",
"plugin:unicorn/recommended",
"prettier"
],
@@ -15,9 +17,15 @@
"warnOnUnsupportedTypeScriptVersion": false,
"sourceType": "unambiguous"
},
"plugins": ["formatjs"],
"plugins": ["formatjs", "no-jquery"],
"settings": {
"additionalFunctionNames": ["$t", "$t_html"]
"additionalFunctionNames": ["$t", "$t_html"],
"no-jquery": {
"collectionReturningPlugins": {
"expectOne": "always"
},
"variablePattern": "^\\$(?!t$|t_html$)."
}
},
"reportUnusedDisableDirectives": true,
"rules": {
@@ -65,6 +73,7 @@
"no-implied-eval": "error",
"no-inner-declarations": "off",
"no-iterator": "error",
"no-jquery/no-parse-html-literal": "error",
"no-label-var": "error",
"no-labels": "error",
"no-loop-func": "error",
@@ -120,6 +129,12 @@
"yoda": "error"
},
"overrides": [
{
"files": ["frontend_tests/node_tests/**", "frontend_tests/zjsunit/**"],
"rules": {
"no-jquery/no-selector-prop": "off"
}
},
{
"files": ["frontend_tests/puppeteer_lib/**", "frontend_tests/puppeteer_tests/**"],
"globals": {

View File

@@ -1,36 +1,73 @@
# This file teaches `git log` and friends the canonical names
# and email addresses to use for our contributors.
#
# For details on the format, see:
# https://git.github.io/htmldocs/gitmailmap.html
#
# Handy commands for examining or adding to this file:
#
# # shows all names/emails after mapping, sorted:
# $ git shortlog -es | sort -k2
#
# # shows raw names/emails, filtered by mapped name:
# $ git log --format='%an %ae' --author=$NAME | uniq -c
Adam Benesh <Adam.Benesh@gmail.com> <Adam-Daniel.Benesh@t-systems.com>
Adam Benesh <Adam.Benesh@gmail.com>
Alex Vandiver <alexmv@zulip.com> <alex@chmrr.net>
Alex Vandiver <alexmv@zulip.com> <github@chmrr.net>
Allen Rabinovich <allenrabinovich@yahoo.com> <allenr@humbughq.com>
Allen Rabinovich <allenrabinovich@yahoo.com> <allenr@zulip.com>
Alya Abbott <alya@zulip.com> <2090066+alya@users.noreply.github.com>
Aman Agrawal <amanagr@zulip.com> <f2016561@pilani.bits-pilani.ac.in>
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
Anders Kaseorg <anders@zulip.com> <andersk@mit.edu>
Aryan Shridhar <aryanshridhar7@gmail.com> <53977614+aryanshridhar@users.noreply.github.com>
Aryan Shridhar <aryanshridhar7@gmail.com>
Ashwat Kumar Singh <ashwat.kumarsingh.met20@itbhu.ac.in>
Austin Riba <austin@zulip.com> <austin@m51.io>
BIKI DAS <bikid475@gmail.com>
Brock Whittaker <brock@zulipchat.com> <bjwhitta@asu.edu>
Brock Whittaker <brock@zulipchat.com> <brockwhittaker@Brocks-MacBook.local>
Brock Whittaker <brock@zulipchat.com> <brock@zulipchat.org>
Chris Bobbe <cbobbe@zulip.com> <cbobbe@zulipchat.com>
Chris Bobbe <cbobbe@zulip.com> <csbobbe@gmail.com>
Eeshan Garg <eeshan@zulip.com> <jerryguitarist@gmail.com>
Greg Price <greg@zulip.com> <gnprice@gmail.com>
Greg Price <greg@zulip.com> <greg@zulipchat.com>
Greg Price <greg@zulip.com> <price@mit.edu>
Jai soni <jai_s@me.iitr.ac.in>
Jai soni <jai_s@me.iitr.ac.in> <76561593+jai2201@users.noreply.github.com>
Jeff Arnold <jbarnold@gmail.com> <jbarnold@humbughq.com>
Jeff Arnold <jbarnold@gmail.com> <jbarnold@zulip.com>
Jessica McKellar <jesstess@mit.edu> <jesstess@humbughq.com>
Jessica McKellar <jesstess@mit.edu> <jesstess@zulip.com>
Julia Bichler <julia.bichler@tum.de> <74348920+juliaBichler01@users.noreply.github.com>
Kevin Mehall <km@kevinmehall.net> <kevin@humbughq.com>
Kevin Mehall <km@kevinmehall.net> <kevin@zulip.com>
Kevin Scott <kevin.scott.98@gmail.com>
Lauryn Menard <lauryn@zulip.com> <lauryn.menard@gmail.com>
Mateusz Mandera <mateusz.mandera@zulip.com> <mateusz.mandera@protonmail.com>
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in>
Palash Raghuwanshi <singhpalash0@gmail.com>
Parth <mittalparth22@gmail.com>
Priyam Seth <sethpriyam1@gmail.com> <b19188@students.iitmandi.ac.in>
Ray Kraesig <rkraesig@zulip.com> <rkraesig@zulipchat.com>
Reid Barton <rwbarton@gmail.com> <rwbarton@humbughq.com>
Rishi Gupta <rishig@zulipchat.com> <rishig+git@mit.edu>
Rishi Gupta <rishig@zulipchat.com> <rishig@kandralabs.com>
Rishi Gupta <rishig@zulipchat.com> <rishig@users.noreply.github.com>
Reid Barton <rwbarton@gmail.com> <rwbarton@humbughq.com>
Rishabh Maheshwari <b20063@students.iitmandi.ac.in>
Sayam Samal <samal.sayam@gmail.com>
Scott Feeney <scott@oceanbase.org> <scott@humbughq.com>
Scott Feeney <scott@oceanbase.org> <scott@zulip.com>
Shlok Patel <shlokcpatel2001@gmail.com>
Steve Howell <showell@zulip.com> <showell30@yahoo.com>
Steve Howell <showell@zulip.com> <showell@yahoo.com>
Steve Howell <showell@zulip.com> <showell@zulipchat.com>
Steve Howell <showell@zulip.com> <steve@humbughq.com>
Steve Howell <showell@zulip.com> <steve@zulip.com>
strifel <info@strifel.de>
Tim Abbott <tabbott@zulip.com> <tabbott@dropbox.com>
Tim Abbott <tabbott@zulip.com> <tabbott@humbughq.com>
Tim Abbott <tabbott@zulip.com> <tabbott@mit.edu>
@@ -41,3 +78,6 @@ Alya Abbott <alya@zulip.com> <alyaabbott@elance-odesk.com>
Sahil Batra <sahil@zulip.com> <sahilbatra839@gmail.com>
Yash RE <33805964+YashRE42@users.noreply.github.com> <YashRE42@github.com>
Yash RE <33805964+YashRE42@users.noreply.github.com>
Yogesh Sirsat <yogeshsirsat56@gmail.com>
Zeeshan Equbal <equbalzeeshan@gmail.com> <54993043+zee-bit@users.noreply.github.com>
Zeeshan Equbal <equbalzeeshan@gmail.com>

View File

@@ -219,6 +219,61 @@ prefix when you think it's time for someone else to review your work.
[git-guide]: https://zulip.readthedocs.io/en/latest/git/
[git-guide-make-pr]: https://zulip.readthedocs.io/en/latest/git/pull-requests.html
### Stages of a pull request
Your pull request will likely go through several stages of review.
1. If your PR makes user-facing changes, the UI and user experience may be
reviewed early on, without reference to the code. You will get feedback on
any user-facing bugs in the implementation. To minimize the number of review
round-trips, make sure to [thoroughly
test](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#manual-testing)
your own PR prior to asking for review.
2. There may be choices made in the implementation that the reviewer
will ask you to revisit. This process will go more smoothly if you
specifically call attention to the decisions you made while
drafting the PR and any points about which you are uncertain. The
PR description and comments on your own PR are good ways to do this.
3. Oftentimes, seeing an initial implementation will make it clear that the
product design for a feature needs to be revised, or that additional changes
are needed. The reviewer may therefore ask you to amend or change the
implementation. Some changes may be blockers for getting the PR merged, while
others may be improvements that can happen afterwards. Feel free to ask if
it's unclear which type of feedback you're getting. (Follow-ups can be a
great next issue to work on!)
4. In addition to any UI/user experience review, all PRs will go through one or
more rounds of code review. Your code may initially be [reviewed by other
contributors](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html).
This helps us make good use of project maintainers' time, and helps you make
progress on the PR by getting more frequent feedback. A project maintainer
may leave a comment asking someone with expertise in the area you're working
on to review your work.
5. Final code review and integration for server and webapp PRs is generally done
by `@timabbott`.
#### How to help move the review process forward
The key to keeping your review moving through the review process is to:
- Address _all_ the feedback to the best of your ability.
- Make it clear when the requested changes have been made
and you believe it's time for another look.
- Make it as easy as possible to review the changes you made.
In order to do this, when you believe you have addressed the previous round of
feedback on your PR as best you can, post an comment asking reviewers to take
another look. Your comment should make it easy to understand what has been done
and what remains by:
- Summarizing the changes made since the last review you received.
- Highlighting remaining questions or decisions, with links to any relevant
chat.zulip.org threads.
- Providing updated screenshots and information on manual testing if
appropriate.
The easier it is to review your work, the more likely you are to receive quick
feedback.
### Beyond the first issue
To find a second issue to work on, we recommend looking through issues with the same
@@ -252,25 +307,18 @@ labels.
1. **Double-check that you have addressed all the feedback**, including any comments
on [Git commit
discipline](https://zulip.readthedocs.io/en/latest/contributing/version-control.html#commit-discipline).
2. If all the feedback has been addressed, did you leave a comment explaining that
you have done so and **requesting another review**? If not, it may not be a
clear to project maintainers that your PR is ready for another look.
3. It is common for PRs to require **multiple rounds of review**. For example,
prior to getting code review from project maintainers, you may receive
feedback on the UI (without regard for the implementation), and your code
may be [reviewed by other
contributors](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html).
This helps us make good use of project maintainers' time, and helps you
make progress on the PR by getting more frequent feedback.
2. If all the feedback has been addressed, did you [leave a
comment](https://zulip.readthedocs.io/en/latest/overview/contributing.html#how-to-help-move-the-review-process-forward)
explaining that you have done so and **requesting another review**? If not,
it may not be clear to project maintainers or reviewers that your PR is
ready for another look.
3. There may be a pause between initial rounds of review for your PR and final
review by project maintainers. This is normal, and we encourage you to **work
on other issues** while you wait.
4. If you think the PR is ready and haven't seen any updates for a couple
of weeks, it can be helpful to post a **comment summarizing your
understanding of the state of the review process**. Your comment should
make it easy to understand what has been done and what remains by:
- Summarizing the changes made since the last review you received.
- Highlighting remaining questions or decisions, with links to any
relevant chat.zulip.org threads.
- Providing updated screenshots and information on manual testing if
appropriate.
of weeks, it can be helpful to **leave another comment**. Summarize the
overall state of the review process and your work, and indicate that you
are waiting for a review.
5. Finally, **Zulip project maintainers are people too**! They may be busy
with other work, and sometimes they might even take a vacation. ;) It can
occasionally take a few weeks for a PR in the final stages of the review

View File

@@ -15,7 +15,6 @@ module.exports = {
"@babel/preset-env",
{
corejs: "3.20",
loose: true, // Loose mode for…of loops are 5× faster in Firefox
shippedProposals: true,
useBuiltIns: "usage",
},

View File

@@ -191,7 +191,7 @@ def read_stripe_fixture(
requestor.interpret_response(
fixture["http_body"], fixture["http_status"], fixture["headers"]
)
return stripe.util.convert_to_stripe_object(fixture) # type: ignore[attr-defined] # missing from stubs
return stripe.util.convert_to_stripe_object(fixture)
return _read_stripe_fixture

View File

@@ -91,7 +91,7 @@ Files: static/third/marked/*
Copyright: 2011-2013, Christopher Jeffrey
License: Expat
Files: static/assets/icons/ellipsis-v-solid.svg
Files: static/shared/icons/ellipsis-v-solid.svg
Copyright: 2017 Fonticons, Inc.
License: CC-BY-4.0
Comment: This icon is picked from Version 5.13.0 of Font Awesome
@@ -113,6 +113,11 @@ Files: static/audio/notification_sounds/*
Copyright: 2021 Stefan Mertens
License: CC0-1.0
Files: static/shared/icons/globe.svg
Source: https://github.com/ionic-team/ionicons/blob/v5.5.2/src/svg/earth.svg, modified to increase the width of the globe outline.
Copyright: 2015-present Ionic (http://ionic.io/)
License: Expat
License: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,11 +1,23 @@
# Version history
This page the release history for the Zulip server. See also the
[Zulip release lifecycle](release-lifecycle.md).
[Zulip release lifecycle](../overview/release-lifecycle.md).
## Zulip 6.x series
### 6.0 -- unreleased
This section is an incomplete draft of the release notes for the next
major release, and is only updated occasionally. See the [commit
log][commit-log] for an up-to-date list of raw changes.
#### Upgrade notes for 6.0
- None yet.
## Zulip 5.x series
### 5.0-rc1 -- March 15, 2022
### 5.0 -- 2022-03-29
This section is an incomplete draft of the release notes for the next
major release, and is only updated occasionally. See the [commit
@@ -42,8 +54,13 @@ log][commit-log] for an up-to-date list of raw changes.
preference settings for new users joining the organization.
- Most permissions settings now support choosing which roles have the
permission, rather than just allowing administrators or everyone.
- Permanent links to conversations now correctly redirect if the
target message has been moved to a new stream or topic.
- Added a data import tool for migrating from Rocket.Chat. Mattermost
data import now supports importing uploaded files.
- Improved handling of messages containing many images; now up to 20
images can be previewed in a single message (up from 5), and a new
grid layout will be used.
- OpenID Connect joins SAML, LDAP, Google, GitHub, Azure Active
Directory, and more as a supported Single Sign-On provider.
- SAML authentication now supports syncing custom profile
@@ -87,6 +104,7 @@ log][commit-log] for an up-to-date list of raw changes.
Sonarr, SonarQube.
- Message edit notifications now indicate how many messages were
moved, when only part of a topic was moved.
- Muted topic records are now moved when an entire topic is moved.
- Search views that don't mark messages as read now have an
explanatory notice if any unread messages are present.
- Added new "Scroll to bottom" widget hovering over the message feed.
@@ -102,7 +120,8 @@ log][commit-log] for an up-to-date list of raw changes.
- Redesigned hover behavior for timestamps and time mentions.
- Messages sent by muted users can now be rehidden after being
revealed. One can also now mute deactivated users.
- Rewrote Help Center guides for new organizations and users.
- Rewrote Help Center guides for new organizations and users, and made
hundreds of other improvements to Help Center content and organization.
- Reimplemented the image lightbox's pan/zoom functionality to be
nicer, allowing us to enable it be default.
- Added styled loading page for the web application.
@@ -110,8 +129,9 @@ log][commit-log] for an up-to-date list of raw changes.
- Notifications now differentiate user group mentions from personal mentions.
- Added support for configuring how long the server should wait before
sending email notifications after a mention or PM.
- Improved integrations: GitHub, Grafana, PagerDuty.
- Improved various interaction details in Recent Topics.
- Improved integrations: BigBlueButton, GitHub, Grafana, PagerDuty,
and many more.
- Improved various interaction and performance details in Recent Topics.
- Improved styling for poll and todo list widgets.
- Zulip now supports configuring the database name and username when
using a remote Postgres server. Previously, these were hardcoded to "zulip".
@@ -169,6 +189,7 @@ log][commit-log] for an up-to-date list of raw changes.
- Fixed a bug where different messages in search results would be
incorrectly shown with a shared recipient bar despite potentially
not being temporally adjacent.
- Fixed lightbox download button not working with the S3 upload backend.
- Increased default retention period before permanently removing
deleted messages from 7 days to 30 days.
- Rate limiting now supports treating all Tor exit nodes as a single IP.
@@ -177,9 +198,13 @@ log][commit-log] for an up-to-date list of raw changes.
software from flagging invitations.
- Added support for uploading animated PNGs as custom emoji.
- Renamed "Night mode" to "Dark theme".
- Added the mobile app's notification sound to desktop sound options,
as "Chime".
- Reworked the `manage.py help` interface to hide Django commands that are
useless or harmful to run on a production system. Also deleted
several useless management commands.
- Improved help and functionality of several management commands. New
create_realm management command supports some automation workflows.
- Added `RealmAuditLog` logging for most administrative actions that
were previously not tracked.
- Added automated testing of the upgrade process from previous releases,
@@ -1551,7 +1576,7 @@ Zulip installations; it has minimal changes for existing servers.
disruption by running this migration first, before beginning the
user-facing downtime. However, if you'd like to watch the downtime
phase of the upgrade closely, we recommend
[running them first manually](https://zulip.readthedocs.io/en/1.9.0/production/expensive-migrations.html)
running them first manually
as well as the usual trick of doing an apt upgrade first.
#### Full feature changelog
@@ -1940,7 +1965,7 @@ running a version from before 1.7 should upgrade directly to 1.7.1.
minimizes disruption by running these first, before beginning the
user-facing downtime. However, if you'd like to watch the downtime
phase of the upgrade closely, we recommend
[running them first manually](https://zulip.readthedocs.io/en/1.9.0/production/expensive-migrations.html)
running them first manually
as well as the usual trick of doing an apt upgrade first.
- We've removed support for an uncommon legacy deployment model where
@@ -2565,15 +2590,17 @@ running a version from before 1.7 should upgrade directly to 1.7.1.
This section links to the upgrade notes from past releases, so you can
easily read them all when upgrading across multiple releases.
- [Draft upgrade notes for 5.0](#upgrade-notes-for-50)
- [Upgrade notes for 4.0](#upgrade-notes-for-40)
- [Upgrade notes for 3.0](#upgrade-notes-for-30)
- [Upgrade notes for 2.1.5](#upgrade-notes-for-215)
- [Upgrade notes for 2.1.0](#upgrade-notes-for-210)
- [Upgrade notes for 2.0.0](#upgrade-notes-for-200)
- [Upgrade notes for 1.9.0](#upgrade-notes-for-190)
- [Upgrade notes for 1.8.0](#upgrade-notes-for-180)
- [Upgrade notes for 1.7.0](#upgrade-notes-for-170)
- [Draft upgrade notes for 6.0](#upgrade-notes-for-60)
* [Upgrade notes for 5.0](#upgrade-notes-for-50)
* [Upgrade notes for 4.0](#upgrade-notes-for-40)
* [Upgrade notes for 3.0](#upgrade-notes-for-30)
* [Upgrade notes for 2.1.5](#upgrade-notes-for-215)
* [Upgrade notes for 2.1.0](#upgrade-notes-for-210)
* [Upgrade notes for 2.0.0](#upgrade-notes-for-200)
* [Upgrade notes for 1.9.0](#upgrade-notes-for-190)
* [Upgrade notes for 1.8.0](#upgrade-notes-for-180)
* [Upgrade notes for 1.7.0](#upgrade-notes-for-170)
[docker-zulip]: https://github.com/zulip/docker-zulip
[commit-log]: https://github.com/zulip/zulip/commits/main

View File

@@ -66,8 +66,8 @@ templating systems.
- `node_modules/` Third-party JavaScript installed via `yarn`.
- `static/assets/` For assets not to be served to the web (e.g. the system to
generate our favicons).
- `static/shared/icons/` Icons placed in this directory are compiled
into an icon font.
---

View File

@@ -289,11 +289,23 @@ AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
You can restrict access to your Zulip server to a set of LDAP groups
using the `AUTH_LDAP_REQUIRE_GROUP` and `AUTH_LDAP_DENY_GROUP`
settings in `/etc/zulip/settings.py`. See the
[upstream django-auth-ldap documentation][upstream-ldap-groups] for
details.
settings in `/etc/zulip/settings.py`.
[upstream-ldap-groups]: https://django-auth-ldap.readthedocs.io/en/latest/groups.html#limiting-access
An example configation for Active Directory group restriction can be:
```
import django_auth_ldap
AUTH_LDAP_GROUP_TYPE = django_auth_ldap.config.ActiveDirectoryGroupType()
AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com"
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=groups,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)")
```
Please note that `AUTH_LDAP_GROUP_TYPE` needs to be set to the correct
group type for your LDAP server. See the [upstream django-auth-ldap
documentation][upstream-ldap-groups] for details.
[upstream-ldap-groups]: https://django-auth-ldap.readthedocs.io/en/latest/groups.html
### Restricting LDAP user access to specific organizations

View File

@@ -274,6 +274,19 @@ class, and configure the `[http_proxy]` block as above.
[smokescreen-acls]: https://github.com/stripe/smokescreen#acls
[ssrf]: https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
### S3 file storage requests and outgoing proxies
By default, the [S3 file storage backend][s3] bypasses the Smokescreen
proxy, because when running on EC2 it may require metadata from the
IMDS metadata endpoint, which resides on the internal IP address
169.254.169.254 and would thus be blocked by Smokescreen.
If your S3-compatible storage backend requires use of Smokescreen or
some other proxy, you can override this default by setting
`S3_SKIP_PROXY = False` in `/etc/zulip/settings.py`.
[s3]: upload-backends.md#s3-backend-configuration
## Putting the Zulip application behind a reverse proxy
Zulip is designed to support being run behind a reverse proxy server.
@@ -512,8 +525,9 @@ deployment. Zulip's configuration builds on top of `wal-g`, our
[database backup solution][wal-g], and thus requires that it be
configured for the primary and all secondary warm standby replicas.
Warm spare replicas should also have `wal-g` backups configured, and
their primary replica and replication username set:
In addition to having `wal-g` backups configured, warm standby
replicas should configure the hostname of their primary replica, and
username to use for replication, in `/etc/zulip/zulip.conf`:
```ini
[postgresql]
@@ -522,14 +536,14 @@ replication_primary = hostname-of-primary.example.com
```
The `postgres` user on the replica will need to be able to
authenticate as the `replicator` user, which may require further
authenticate as the `replication_user` user, which may require further
configuration of `pg_hba.conf` and client certificates on the replica.
If you are using password authentication, you can set a
`postgresql_replication_password` secret in
`/etc/zulip/zulip-secrets.conf`.
[warm-standby]: https://www.postgresql.org/docs/current/warm-standby.html
[wal-g]: export-and-import.md#backup-details
[wal-g]: export-and-import.md#database-only-backup-tools
## System and deployment configuration

View File

@@ -161,25 +161,9 @@ included in the backups generated by Zulip's standard tools. The
data includes:
- The PostgreSQL database. You can back this up with any standard
database export or backup tool. Zulip has built-in support for taking
daily incremental backups using
[wal-g](https://github.com/wal-g/wal-g); these backups are stored for
30 days in S3. If you have an Amazon S3 bucket you wish to store for
storing the backups, edit `/etc/zulip/zulip-secrets.conf` on the
PostgreSQL server to add:
```ini
s3_region = # region to write to S3; defaults to EC2 host's region
s3_backups_key = # aws public key; optional, if access not through role
s3_backups_secret_key = # aws secret key; optional, if access not through role
s3_backups_bucket = # name of S3 backup bucket
```
After adding the secrets, run
`/home/zulip/deployments/current/scripts/zulip-puppet-apply`. You
can (and should) monitor that backups are running regularly via
the Nagios plugin installed into
`/usr/lib/nagios/plugins/zulip_postgresql_backups/check_postgresql_backup`.
database export or backup tool; see
[below](#database-only-backup-tools) for Zulip's built-in support
for continuous point-in-time backups.
- Any user-uploaded files. If you're using S3 as storage for file
uploads, this is backed up in S3. But if you have instead set
@@ -201,16 +185,12 @@ data includes:
To restore from a manual backup, the process is basically the reverse of the above:
- Install new server as normal by downloading a Zulip release tarball
and then using `scripts/setup/install`. You don't need
to run the `initialize-database` second stage which puts default
data into the database.
and then using `scripts/setup/install`. You should pass
`--no-init-db` because we don't need to create a new database.
- Unpack to `/etc/zulip` the `settings.py` and `zulip-secrets.conf` files
from your backups.
- If you ran `initialize-database` anyway above, you'll want to run
`scripts/setup/postgresql-init-db` to drop the initial database first.
- Restore your database from the backup.
- Reconfigure rabbitmq to use the password from `secrets.conf`
@@ -359,7 +339,7 @@ cd ~
tar -xf /path/to/export/file/zulip-export-zcmpxfm6.tar.gz
cd /home/zulip/deployments/current
./manage.py import '' ~/zulip-export-zcmpxfm6
# supervisorctl start all # Starts the Zulip server
# ./scripts/start-server
# ./manage.py reactivate_realm -r '' # Reactivates the organization
```
@@ -451,3 +431,40 @@ rm -rf /home/zulip/uploads/*/2/
```
Once that's done, you can simply re-run the import process.
## Database-only backup tools
The [Zulip-specific backup tool documented above](#backups) is perfect
for an all-in-one backup solution, and can be used for nightly
backups. For administrators wanting continuous point-in-time backups,
Zulip has built-in support for taking daily incremental backups using
[wal-g](https://github.com/wal-g/wal-g) and storing them in Amazon S3.
By default, these backups are stored for 30 days.
Note these database backups, by themselves, do not constitute a full
backup of the Zulip system! [See above](#backup-details) for other
pieces which are necessary to back up a Zulip system.
To enable continuous point-in-time backups:
1. Edit `/etc/zulip/zulip-secrets.conf` on the
PostgreSQL server to add:
```ini
s3_region = # region to write to S3; defaults to EC2 host's region
s3_backups_key = # aws public key; optional, if access not through role
s3_backups_secret_key = # aws secret key; optional, if access not through role
s3_backups_bucket = # name of S3 backup bucket
```
1. Run `/home/zulip/deployments/current/scripts/zulip-puppet-apply`.
Daily full-database backups will be taken at 0200 UTC, and every WAL
log will be written to S3 as it is saved by PostgreSQL; by default,
this happens every 5 minutes, or every 1G, whichever happens first.
If you need always-current backup availability, Zulip also has
[built-in database replication support](deployment.md#postgresql-warm-standby).
You can (and should) monitor that backups are running regularly via
the Nagios plugin installed into
`/usr/lib/nagios/plugins/zulip_postgresql_backups/check_postgresql_backup`.

View File

@@ -16,7 +16,7 @@ cd /home/zulip/deployments/current
# Start by reading the help
./manage.py <command_name> --help
# Once you've determine this is the command for you, run it!
# Once you've determined this is the command for you, run it!
./manage.py <command_name> <args>
```

View File

@@ -115,7 +115,7 @@ to these terms.
We've designed this push notification bouncer service with security
and privacy in mind:
- A central design goal of the the Push Notification Service is to
- A central design goal of the Push Notification Service is to
avoid any message content being stored or logged by the service,
even in error cases.
- The Push Notification Service only stores the necessary metadata for
@@ -237,7 +237,7 @@ the app stores yourself.
If you've done that work, the Zulip server configuration for sending
push notifications through the new app is quite straightforward:
- Create a
- Create an
[FCM push notifications](https://firebase.google.com/docs/cloud-messaging)
key in the Google Developer console and set `android_gcm_api_key` in
`/etc/zulip/zulip-secrets.conf` to that key.

View File

@@ -7,9 +7,9 @@ To run a Zulip server, you will need:
- Ubuntu 20.04 Focal
- Debian 11 Bullseye
- Debian 10 Buster
- At least 2GB RAM, and 10GB disk space
- If you expect 100+ users: 4GB RAM, and 2 CPUs
- If you intend to [upgrade from Git][upgrade-from-git]: 3GB RAM, or
- At least 2 GB RAM, and 10 GB disk space
- If you expect 100+ users: 4 GB RAM, and 2 CPUs
- If you intend to [upgrade from Git][upgrade-from-git]: 3 GB RAM, or
2G and at least 1G of swap configured.
- A hostname in DNS
- Credentials for sending email
@@ -59,14 +59,14 @@ sudo apt update
#### Hardware specifications
- CPU and memory: For installations with 100+ users you'll need a
minimum of **2 CPUs** and **4GB RAM**. For installations with fewer
users, 1 CPU and 2GB RAM is sufficient. We strongly recommend against
installing with less than 2GB of RAM, as you will likely experience
minimum of **2 CPUs** and **4 GB RAM**. For installations with fewer
users, 1 CPU and 2 GB RAM is sufficient. We strongly recommend against
installing with less than 2 GB of RAM, as you will likely experience
out of memory issues installing dependencies. We recommend against
using highly CPU-limited servers like the AWS `t2` style instances
for organizations with hundreds of users (active or no).
- Disk space: You'll need at least 10GB of free disk space for a
- Disk space: You'll need at least 10 GB of free disk space for a
server with dozens of users. We recommend using an SSD and avoiding
cloud storage backends that limit the IOPS per second, since the
disk is primarily used for the Zulip database.
@@ -104,7 +104,7 @@ on hardware requirements for larger organizations.
address as its external hostname (though we don't recommend that
configuration).
- Zulip supports [running behind a reverse proxy][reverse-proxy].
- Zulip configures [Smokescreen, and outgoing HTTP
- Zulip configures [Smokescreen, an outgoing HTTP
proxy][smokescreen-proxy], to protect against [SSRF attacks][ssrf],
which prevents user from making the Zulip server make requests to
private resources. If your network has its own outgoing HTTP proxy,
@@ -176,11 +176,11 @@ installing Zulip with a dedicated database server.
- **RAM:** We recommended more RAM for larger installations:
- With 25+ daily active users, 4GB of RAM.
- With 100+ daily active users, 8GB of RAM.
- With 400+ daily active users, 16GB of RAM for the Zulip
application server, plus 16GB for the database.
- With 2000+ daily active users 32GB of RAM, plus 32GB for the
- With 25+ daily active users, 4 GB of RAM.
- With 100+ daily active users, 8 GB of RAM.
- With 400+ daily active users, 16 GB of RAM for the Zulip
application server, plus 16 GB for the database.
- With 2000+ daily active users 32 GB of RAM, plus 32 GB for the
database.
- Roughly linear scaling beyond that.
@@ -195,23 +195,23 @@ installing Zulip with a dedicated database server.
- **Disk for application server:** We recommend using [the S3 file
uploads backend][s3-uploads] to store uploaded files at scale. With
the S3 backend configuration, we recommend 50GB of disk for the OS,
the S3 backend configuration, we recommend 50 GB of disk for the OS,
Zulip software, logs and scratch/free space. Disk needs when
storing uploads locally
- **Disk for database:** SSD disk is highly recommended. For
installations where most messages have <100 recipients, 10GB per 1M
messages of history is sufficient plus 1GB per 1000 users is
installations where most messages have <100 recipients, 10 GB per 1M
messages of history is sufficient plus 1 GB per 1000 users is
sufficient. If most messages are to public streams with 10K+ users
subscribed (like on chat.zulip.org), add 20GB per (1000 user
subscribed (like on chat.zulip.org), add 20 GB per (1000 user
accounts) per (1M messages to public streams).
- **Example:** When
[the Zulip development community](https://zulip.com/development-community/) server
had 12K user accounts (~300 daily actives) and 800K messages of
history (400K to public streams), it was a default configuration
single-server installation with 16GB of RAM, 4 cores (essentially
always idle), and its database was using about 100GB of disk.
single-server installation with 16 GB of RAM, 4 cores (essentially
always idle), and its database was using about 100 GB of disk.
- **Disaster recovery:** One can easily run a warm spare application
server and a warm spare database (using [PostgreSQL warm standby

View File

@@ -47,7 +47,6 @@ When everything is running as expected, you will see something like this:
```console
process-fts-updates RUNNING pid 11392, uptime 19:40:06
smokescreen RUNNING pid 3113, uptime 29 days, 21:58:32
teleport_node RUNNING pid 15683, uptime 3 days, 13:01:58
zulip-django RUNNING pid 11441, uptime 19:39:57
zulip-tornado RUNNING pid 11397, uptime 19:40:03
zulip_deliver_scheduled_emails RUNNING pid 10289, uptime 19:41:04
@@ -77,27 +76,29 @@ isn't running. If you don't see relevant logs in
`/etc/supervisor/conf.d/zulip.conf` for details. Logs only make it to
`/var/log/zulip/errors.log` once a service has started fully.
### Restarting services with `supervisorctl restart all`
### Restarting services with `supervisorctl restart`
After you change configuration in `/etc/zulip/settings.py` or fix a
misconfiguration, you will often want to restart the Zulip application.
You can restart Zulip using:
misconfiguration, you will often want to restart the Zulip
application. Running `scripts/restart-server` will restart all of
Zulip's services; if you want to restart just one of them, you can use
`supervisorctl`:
```bash
supervisorctl restart all
# You can use this for any service found in `supervisorctl list`
supervisorctl restart zulip-django
```
### Stopping services with `supervisorctl stop all`
### Stopping services with `supervisorctl stop`
Similarly, you can stop Zulip using:
Similarly, while stopping all of Zulip is best done by running
`scripts/stop-server`, you can stop individual Zulip services using:
```bash
supervisorctl stop all
# You can use this for any service found in `supervisorctl list`
supervisorctl stop zulip-django
```
If you're looking to shut down the server, it is often better to run
`./scripts/stop-server`.
## Troubleshooting services
The Zulip application uses several major open source services to store

View File

@@ -174,7 +174,7 @@ after resolving an issue. The most common causes of errors are:
minimal RAM for running Zulip can run into out-of-memory issues
during the upgrade process (generally `tools/webpack` is the step
that fails). You can get past this by shutting down the Zulip
server with `supervisorctl stop all` to free up RAM before running
server with `./scripts/stop-server` to free up RAM before running
the upgrade process.
Useful logs are available in a few places:
@@ -419,7 +419,7 @@ instructions for other supported platforms.
to back up the system:
```bash
supervisorctl stop all
/home/zulip/deployments/current/scripts/stop-server
/home/zulip/deployments/current/manage.py backup --output=/home/zulip/release-upgrade.backup.tar.gz
```
@@ -757,8 +757,8 @@ Many Zulip servers (including chat.zulip.org and zulip.com) upgrade to
so, it's important to understand how to happily run a server based on
`main`.
For background, it's backporting arbitrary patches from `main` to an
older version requires some care. Common issues include:
For background, backporting arbitrary patches from `main` to an older
version requires some care. Common issues include:
- Changes containing database migrations (new files under
`*/migrations/`), which includes most new features. We

View File

@@ -28,6 +28,8 @@ backend. To enable this backend, you need to do the following:
1. Set `s3_key` and `s3_secret_key` in /etc/zulip/zulip-secrets.conf
to be the S3 access and secret keys for the IAM account.
Alternately, if your Zulip server runs on an EC2 instance, set the
IAM role for the EC2 instance to the role.
1. Set the `S3_AUTH_UPLOADS_BUCKET` and `S3_AVATAR_BUCKET` settings in
`/etc/zulip/settings.py` to be the names of the S3 buckets you

View File

@@ -18,9 +18,9 @@ The pills will automatically be inserted in before the ".input" in order.
## Basic usage
```js
var pill_containter = $("#input_container");
var $pill_containter = $("#input_container");
var pills = input_pill.create({
container: pill_container,
$container: $pill_container,
create_item_from_text: user_pill.create_item_from_email,
get_text_from_item: user_pill.get_email_from_item,
});

View File

@@ -39,10 +39,10 @@ preparing a new release.
release, on Ubuntu 20.04.
- Repeat until release is ready.
- Send around the Paper blog post draft for review.
- Move the blog post draft to Ghost. (For a draft in Dropbox Paper,
use "··· > Export > Markdown" to get a pretty good markup
conversion.) Proofread the post, especially for formatting. Tag
the post with "Release announcements" in Ghost.
- Move the blog post draft to Ghost:
- Use "··· > Export > Markdown" to get a pretty good markdown conversion, then insert that as a Markdown block in Ghost.
- Proofread, especially for formatting.
- Tag the post with "Release announcements" _first_, then any other tags (e.g. "Security").
### Executing the release
@@ -50,6 +50,7 @@ preparing a new release.
release branch (for minor releases):
- Copy the Markdown release notes for the release into
`docs/overview/changelog.md`.
- Verify the changelog passes lint, and has the right release date.
- _Except minor releases:_ Adjust the `changelog.md` heading to have
the stable release series boilerplate.
- Update `ZULIP_VERSION` and `LATEST_RELEASE_VERSION` in `version.py`.
@@ -67,23 +68,36 @@ preparing a new release.
for updating DigitalOcean one-click app image. The action uses the latest release
tarball published on `download.zulip.com` for creating the image.
- Update the [Docker image](https://github.com/zulip/docker-zulip) and
do a release of that.
- Update the image of DigitalOcean one click app using
[Fabric](https://github.com/zulip/marketplace-partners) and publish
it to DO marketplace.
- Update the [Docker image](https://github.com/zulip/docker-zulip):
- Update `ZULIP_GIT_REF` in `Dockerfile`
- Update `README.md`
- Update the image in `docker-compose.yml`, as well as the `ZULIP_GIT_REF`
- Update the image in `kubernetes/zulip-rc.yml`
- Build the image: `docker build . -t zulip/docker-zulip:4.11-0 --no-cache`
- Also tag it with `latest`: `docker build . -t zulip/docker-zulip:latest`
- Push those tags: `docker push zulip/docker-zulip:4.11-0; docker push zulip/docker-zulip:latest`
- Update the latest version in [the README in Docker Hub](https://hub.docker.com/repository/docker/zulip/docker-zulip).
- Commit the changes and push them to `main`.
- Publish the blog post; check the box to "send by email."
- Email [zulip-announce](https://groups.google.com/g/zulip-announce),
post to [#announce](https://chat.zulip.org/#narrow/stream/1-announce),
and [send a tweet](https://twitter.com/zulip).
- Announce the release, pointing to the blog post, via:
- Email to [zulip-announce](https://groups.google.com/g/zulip-announce)
- Message in [#announce](https://chat.zulip.org/#narrow/stream/1-announce)
- Tweet from [@zulip](https://twitter.com/zulip).
### Post-release
- Update the CI targets in `.github/workflows/production-suite.yml` to
include upgrades from the most recent point releases from the last
two series -- e.g after releasing 4.8, both `main` and `4.x`
should test upgrades from 3.4 and 4.8, and after releasing 5.0 both
`main` and `5.x` should test upgrades from 4.8 and 5.0.
- The DigitalOcean one-click image will report in an internal channel
once it is built, and how to test it. Verify it, then publish it
publish it to DigitalOcean marketplace.
- Update the CI targets:
- _For major releases only:_ In all of the following steps, _also_
bump up the series that are being tested.
- Update the version in `tools/ci/build-docker-images`
- Run `tools/ci/build-docker-images`
- Push at least the latest of those, e.g. using `docker push zulip/ci:bullseye-4.11`; update the others at your discretion.
- Update the `docker_image` in the `production_upgrade` step of
`.github/workflows/production-suite.yml`.
- Commit those two changes in a PR.
- Following a major release (e.g. 4.0):
- Create a release branch (e.g. `4.x`).
- On the release branch, update `ZULIP_VERSION` in `version.py` to
@@ -95,8 +109,9 @@ preparing a new release.
number for future Cloud deployments.
- Consider removing a few old releases from ReadTheDocs; we keep about
two years of back-versions.
- Following a minor release (e.g. 3.2), on the release branch:
- Update `ZULIP_VERSION` to the present release with a `+git`
suffix, e.g. `3.2+git`.
- Update `LATEST_RELEASE_VERSION` with the released version.
- Cherry-pick the changelog changes back to `main`.
- Following a minor release (e.g. 3.2):
- On the release branch, update `ZULIP_VERSION` to the present
release with a `+git` suffix, e.g. `3.2+git`.
- On main, update `LATEST_RELEASE_VERSION` with the released version.
- On main, cherry-pick the changelog changes from the release
branch.

View File

@@ -312,7 +312,7 @@ The code in `static/js/zform.js` renders the form (not
shown here) and then sets up a click handler like below:
```js
elem.find('button').on('click', function (e) {
$elem.find('button').on('click', function (e) {
e.stopPropagation();
// Grab our index from the markup.

View File

@@ -8,8 +8,8 @@ const blueslip = require("../zjsunit/zblueslip");
const $ = require("../zjsunit/zjquery");
const {page_params, user_settings} = require("../zjsunit/zpage_params");
const window_stub = $.create("window-stub");
set_global("to_$", () => window_stub);
const $window_stub = $.create("window-stub");
set_global("to_$", () => $window_stub);
$(window).idle = () => {};
const _document = {
@@ -242,34 +242,34 @@ function simulate_right_column_buddy_list() {
};
}
function buddy_list_add(user_id, stub) {
if (stub.attr) {
stub.attr("data-user-id", user_id);
function buddy_list_add(user_id, $stub) {
if ($stub.attr) {
$stub.attr("data-user-id", user_id);
}
stub.length = 1;
$stub.length = 1;
const sel = `li.user_sidebar_entry[data-user-id='${CSS.escape(user_id)}']`;
$("#user_presences").set_find_results(sel, stub);
$("#user_presences").set_find_results(sel, $stub);
}
test("PM_update_dom_counts", () => {
const count = $.create("alice-unread-count");
const $count = $.create("alice-unread-count");
const pm_key = alice.user_id.toString();
const li = $.create("alice stub");
buddy_list_add(pm_key, li);
li.set_find_results(".unread_count", count);
count.set_parents_result("li", li);
const $li = $.create("alice stub");
buddy_list_add(pm_key, $li);
$li.set_find_results(".unread_count", $count);
$count.set_parents_result("li", $li);
const counts = new Map();
counts.set(pm_key, 5);
li.addClass("user_sidebar_entry");
$li.addClass("user_sidebar_entry");
activity.update_dom_with_unread_counts({pm_count: counts});
assert.equal(count.text(), "5");
assert.equal($count.text(), "5");
counts.set(pm_key, 0);
activity.update_dom_with_unread_counts({pm_count: counts});
assert.equal(count.text(), "");
assert.equal($count.text(), "");
});
test("handlers", ({override, override_rewire, mock_template}) => {
@@ -289,9 +289,9 @@ test("handlers", ({override, override_rewire, mock_template}) => {
// This is kind of weak coverage; we are mostly making sure that
// keys and clicks got mapped to functions that don't crash.
let me_li;
let alice_li;
let fred_li;
let $me_li;
let $alice_li;
let $fred_li;
let narrowed;
@@ -307,13 +307,13 @@ test("handlers", ({override, override_rewire, mock_template}) => {
});
activity.set_cursor_and_filter();
me_li = $.create("me stub");
alice_li = $.create("alice stub");
fred_li = $.create("fred stub");
$me_li = $.create("me stub");
$alice_li = $.create("alice stub");
$fred_li = $.create("fred stub");
buddy_list_add(me.user_id, me_li);
buddy_list_add(alice.user_id, alice_li);
buddy_list_add(fred.user_id, fred_li);
buddy_list_add(me.user_id, $me_li);
buddy_list_add(alice.user_id, $alice_li);
buddy_list_add(fred.user_id, $fred_li);
}
(function test_filter_keys() {
@@ -364,7 +364,7 @@ test("handlers", ({override, override_rewire, mock_template}) => {
// We wire up the click handler in click_handlers.js,
// so this just tests the called function.
narrowed = false;
activity.narrow_for_user({li: alice_li});
activity.narrow_for_user({$li: $alice_li});
assert.ok(narrowed);
})();
@@ -420,7 +420,7 @@ test("first/prev/next", ({override, mock_template}) => {
assert.equal(buddy_list.prev_key(alice.user_id), undefined);
assert.equal(buddy_list.next_key(alice.user_id), undefined);
override(buddy_list.container, "append", () => {});
override(buddy_list.$container, "append", () => {});
activity.redraw_user(alice.user_id);
activity.redraw_user(fred.user_id);
@@ -457,7 +457,7 @@ test("insert_one_user_into_empty_list", ({override, mock_template}) => {
override(padded_widget, "update_padding", () => {});
let appended_html;
override(buddy_list.container, "append", (html) => {
override(buddy_list.$container, "append", (html) => {
appended_html = html;
});
@@ -470,7 +470,7 @@ test("insert_alice_then_fred", ({override, mock_template}) => {
mock_template("user_presence_row.hbs", true, (data, html) => html);
let appended_html;
override(buddy_list.container, "append", (html) => {
override(buddy_list.$container, "append", (html) => {
appended_html = html;
});
override(padded_widget, "update_padding", () => {});
@@ -488,7 +488,7 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => {
mock_template("user_presence_row.hbs", true, (data, html) => html);
let appended_html;
override(buddy_list.container, "append", (html) => {
override(buddy_list.$container, "append", (html) => {
appended_html = html;
});
override(padded_widget, "update_padding", () => {});
@@ -497,16 +497,16 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => {
assert.ok(appended_html.indexOf('data-user-id="2"') > 0);
assert.ok(appended_html.indexOf("user_circle_green") > 0);
const fred_stub = $.create("fred-first");
buddy_list_add(fred.user_id, fred_stub);
const $fred_stub = $.create("fred-first");
buddy_list_add(fred.user_id, $fred_stub);
let inserted_html;
fred_stub.before = (html) => {
$fred_stub.before = (html) => {
inserted_html = html;
};
let fred_removed;
fred_stub.remove = () => {
$fred_stub.remove = () => {
fred_removed = true;
};
@@ -522,10 +522,10 @@ test("insert_fred_then_alice_then_rename", ({override, mock_template}) => {
};
people.add_active_user(fred_with_new_name);
const alice_stub = $.create("alice-first");
buddy_list_add(alice.user_id, alice_stub);
const $alice_stub = $.create("alice-first");
buddy_list_add(alice.user_id, $alice_stub);
alice_stub.before = (html) => {
$alice_stub.before = (html) => {
inserted_html = html;
};
@@ -541,8 +541,8 @@ test("insert_unfiltered_user_with_filter", () => {
// This test only tests that we do not explode when
// try to insert Fred into a list where he does not
// match the search filter.
const user_filter = $(".user-list-filter");
user_filter.val("do-not-match-filter");
const $user_filter = $(".user-list-filter");
$user_filter.val("do-not-match-filter");
activity.redraw_user(fred.user_id);
});
@@ -579,8 +579,8 @@ test("update_presence_info", ({override, override_rewire}) => {
override_rewire(buddy_data, "matches_filter", () => true);
const alice_li = $.create("alice stub");
buddy_list_add(alice.user_id, alice_li);
const $alice_li = $.create("alice stub");
buddy_list_add(alice.user_id, $alice_li);
let inserted;
override(buddy_list, "insert_or_move", () => {
@@ -613,8 +613,8 @@ test("initialize", ({override, mock_template}) => {
function clear() {
$.clear_all_elements();
buddy_list.container = $("#user_presences");
buddy_list.container.append = () => {};
buddy_list.$container = $("#user_presences");
buddy_list.$container.append = () => {};
clear_buddy_list();
page_params.presences = {};
}

View File

@@ -50,31 +50,31 @@ run_test("add_alert_word", ({override_rewire}) => {
alert_words_ui.set_up_alert_words();
const create_form = $("#create_alert_word_form");
const add_func = create_form.get_on_handler("click", "#create_alert_word_button");
const $create_form = $("#create_alert_word_form");
const add_func = $create_form.get_on_handler("click", "#create_alert_word_button");
const new_alert_word = $("#create_alert_word_name");
const alert_word_status = $("#alert_word_status");
const alert_word_status_text = $(".alert_word_status_text");
alert_word_status.set_find_results(".alert_word_status_text", alert_word_status_text);
const $new_alert_word = $("#create_alert_word_name");
const $alert_word_status = $("#alert_word_status");
const $alert_word_status_text = $(".alert_word_status_text");
$alert_word_status.set_find_results(".alert_word_status_text", $alert_word_status_text);
// add '' as alert word
add_func();
assert.equal(new_alert_word.val(), "");
assert.ok(alert_word_status.hasClass("alert-danger"));
assert.equal(alert_word_status_text.text(), "translated: Alert word can't be empty!");
assert.ok(alert_word_status.visible());
assert.equal($new_alert_word.val(), "");
assert.ok($alert_word_status.hasClass("alert-danger"));
assert.equal($alert_word_status_text.text(), "translated: Alert word can't be empty!");
assert.ok($alert_word_status.visible());
// add 'foo' as alert word (existing word)
new_alert_word.val("foo");
$new_alert_word.val("foo");
add_func();
assert.ok(alert_word_status.hasClass("alert-danger"));
assert.equal(alert_word_status_text.text(), "translated: Alert word already exists!");
assert.ok(alert_word_status.visible());
assert.ok($alert_word_status.hasClass("alert-danger"));
assert.equal($alert_word_status_text.text(), "translated: Alert word already exists!");
assert.ok($alert_word_status.visible());
// add 'zot' as alert word (new word)
new_alert_word.val("zot");
$new_alert_word.val("zot");
let success_func;
let fail_func;
@@ -89,26 +89,29 @@ run_test("add_alert_word", ({override_rewire}) => {
// test failure
fail_func();
assert.ok(alert_word_status.hasClass("alert-danger"));
assert.equal(alert_word_status_text.text(), "translated: Error adding alert word!");
assert.ok(alert_word_status.visible());
assert.ok($alert_word_status.hasClass("alert-danger"));
assert.equal($alert_word_status_text.text(), "translated: Error adding alert word!");
assert.ok($alert_word_status.visible());
// test success
success_func();
assert.ok(alert_word_status.hasClass("alert-success"));
assert.equal(alert_word_status_text.text(), 'translated: Alert word "zot" added successfully!');
assert.ok(alert_word_status.visible());
assert.ok($alert_word_status.hasClass("alert-success"));
assert.equal(
$alert_word_status_text.text(),
'translated: Alert word "zot" added successfully!',
);
assert.ok($alert_word_status.visible());
});
run_test("add_alert_word_keypress", ({override_rewire}) => {
override_rewire(alert_words_ui, "rerender_alert_words_ui", () => {});
alert_words_ui.set_up_alert_words();
const create_form = $("#create_alert_word_form");
const keypress_func = create_form.get_on_handler("keypress", "#create_alert_word_name");
const $create_form = $("#create_alert_word_form");
const keypress_func = $create_form.get_on_handler("keypress", "#create_alert_word_name");
const new_alert_word = $("#create_alert_word_name");
new_alert_word.val("zot");
const $new_alert_word = $("#create_alert_word_name");
$new_alert_word.val("zot");
const event = {
preventDefault: () => {},
@@ -130,16 +133,16 @@ run_test("remove_alert_word", ({override_rewire}) => {
override_rewire(alert_words_ui, "rerender_alert_words_ui", () => {});
alert_words_ui.set_up_alert_words();
const word_list = $("#alert-words-table");
const remove_func = word_list.get_on_handler("click", ".remove-alert-word");
const $word_list = $("#alert-words-table");
const remove_func = $word_list.get_on_handler("click", ".remove-alert-word");
const remove_alert_word = $(".remove-alert-word");
const list_item = $("tr.alert-word-item");
const val_item = $("span.value");
val_item.text($t({defaultMessage: "zot"}));
const $remove_alert_word = $(".remove-alert-word");
const $list_item = $("tr.alert-word-item");
const $val_item = $("span.value");
$val_item.text($t({defaultMessage: "zot"}));
remove_alert_word.set_parents_result("tr", list_item);
list_item.set_find_results(".value", val_item);
$remove_alert_word.set_parents_result("tr", $list_item);
$list_item.set_find_results(".value", $val_item);
const event = {
currentTarget: ".remove-alert-word",
@@ -156,42 +159,42 @@ run_test("remove_alert_word", ({override_rewire}) => {
remove_func(event);
const alert_word_status = $("#alert_word_status");
const alert_word_status_text = $(".alert_word_status_text");
alert_word_status.set_find_results(".alert_word_status_text", alert_word_status_text);
const $alert_word_status = $("#alert_word_status");
const $alert_word_status_text = $(".alert_word_status_text");
$alert_word_status.set_find_results(".alert_word_status_text", $alert_word_status_text);
// test failure
fail_func();
assert.ok(alert_word_status.hasClass("alert-danger"));
assert.equal(alert_word_status_text.text(), "translated: Error removing alert word!");
assert.ok(alert_word_status.visible());
assert.ok($alert_word_status.hasClass("alert-danger"));
assert.equal($alert_word_status_text.text(), "translated: Error removing alert word!");
assert.ok($alert_word_status.visible());
// test success
success_func();
assert.ok(alert_word_status.hasClass("alert-success"));
assert.equal(alert_word_status_text.text(), "translated: Alert word removed successfully!");
assert.ok(alert_word_status.visible());
assert.ok($alert_word_status.hasClass("alert-success"));
assert.equal($alert_word_status_text.text(), "translated: Alert word removed successfully!");
assert.ok($alert_word_status.visible());
});
run_test("close_status_message", ({override_rewire}) => {
override_rewire(alert_words_ui, "rerender_alert_words_ui", () => {});
alert_words_ui.set_up_alert_words();
const alert_word_settings = $("#alert-word-settings");
const close = alert_word_settings.get_on_handler("click", ".close-alert-word-status");
const $alert_word_settings = $("#alert-word-settings");
const close = $alert_word_settings.get_on_handler("click", ".close-alert-word-status");
const alert = $(".alert");
const close_btn = $(".close-alert-word-status");
close_btn.set_parents_result(".alert", alert);
const $alert = $(".alert");
const $close_btn = $(".close-alert-word-status");
$close_btn.set_parents_result(".alert", $alert);
alert.show();
$alert.show();
const event = {
preventDefault: () => {},
currentTarget: ".close-alert-word-status",
};
assert.ok(alert.visible());
assert.ok($alert.visible());
close(event);
assert.ok(!alert.visible());
assert.ok(!$alert.visible());
});

View File

@@ -52,8 +52,8 @@ run_test("create_ajax_request", ({override}) => {
make_indicator: 0,
};
loading.make_indicator = (loading_indicator, config) => {
assert.equal(loading_indicator.selector, form_loading_indicator);
loading.make_indicator = ($loading_indicator, config) => {
assert.equal($loading_indicator.selector, form_loading_indicator);
assert.equal(config.text, "Processing ...");
assert.equal(config.abs_positioned, true);
state.make_indicator += 1;

View File

@@ -39,18 +39,18 @@ people.add_active_user(alice);
run_test("get_items", () => {
const buddy_list = new BuddyList();
// We don't make alice_li an actual jQuery stub,
// We don't make $alice_li an actual jQuery stub,
// because our test only cares that it comes
// back from get_items.
const alice_li = "alice stub";
const $alice_li = "alice stub";
const sel = "li.user_sidebar_entry";
const container = $.create("get_items container", {
children: [{to_$: () => alice_li}],
const $container = $.create("get_items container", {
children: [{to_$: () => $alice_li}],
});
buddy_list.container.set_find_results(sel, container);
buddy_list.$container.set_find_results(sel, $container);
const items = buddy_list.get_items();
assert.deepEqual(items, [alice_li]);
assert.deepEqual(items, [$alice_li]);
});
run_test("basics", ({override}) => {
@@ -83,19 +83,19 @@ run_test("basics", ({override}) => {
});
assert.ok(appended);
const alice_li = {length: 1};
const $alice_li = {length: 1};
override(buddy_list, "get_li_from_key", (opts) => {
const key = opts.key;
assert.equal(key, alice.user_id);
return alice_li;
return $alice_li;
});
const li = buddy_list.find_li({
const $li = buddy_list.find_li({
key: alice.user_id,
});
assert.equal(li, alice_li);
assert.equal($li, $alice_li);
});
run_test("big_list", ({override}) => {
@@ -163,11 +163,11 @@ run_test("find_li w/force_render", ({override}) => {
// key is not already rendered in DOM, then the
// widget will call show_key to force-render it.
const key = "999";
const stub_li = {length: 0};
const $stub_li = {length: 0};
override(buddy_list, "get_li_from_key", (opts) => {
assert.equal(opts.key, key);
return stub_li;
return $stub_li;
});
buddy_list.keys = ["foo", "bar", key, "baz"];
@@ -179,18 +179,18 @@ run_test("find_li w/force_render", ({override}) => {
shown = true;
});
const empty_li = buddy_list.find_li({
const $empty_li = buddy_list.find_li({
key,
});
assert.equal(empty_li, stub_li);
assert.equal($empty_li, $stub_li);
assert.ok(!shown);
const li = buddy_list.find_li({
const $li = buddy_list.find_li({
key,
force_render: true,
});
assert.equal(li, stub_li);
assert.equal($li, $stub_li);
assert.ok(shown);
});
@@ -198,12 +198,12 @@ run_test("find_li w/bad key", ({override}) => {
const buddy_list = new BuddyList();
override(buddy_list, "get_li_from_key", () => ({length: 0}));
const undefined_li = buddy_list.find_li({
const $undefined_li = buddy_list.find_li({
key: "not-there",
force_render: true,
});
assert.deepEqual(undefined_li, []);
assert.deepEqual($undefined_li, []);
});
run_test("scrolling", ({override}) => {

View File

@@ -39,41 +39,41 @@ run_test("phrase_match", () => {
run_test("copy_data_attribute_value", ({override}) => {
const admin_emails_val = "iago@zulip.com";
const input = $.create("input");
const $input = $.create("input");
let removed;
input.remove = () => {
$input.remove = () => {
removed = true;
};
override(document, "createElement", () => input);
override(document, "createElement", () => $input);
override(document, "execCommand", noop);
$("body").append = noop;
$(input).val = (arg) => {
$($input).val = (arg) => {
assert.equal(arg, admin_emails_val);
return {
trigger: noop,
};
};
const elem = {};
const $elem = {};
let faded_in = false;
let faded_out = false;
elem.data = (key) => {
$elem.data = (key) => {
assert.equal(key, "admin-emails");
return admin_emails_val;
};
elem.fadeOut = (val) => {
$elem.fadeOut = (val) => {
assert.equal(val, 250);
faded_out = true;
};
elem.fadeIn = (val) => {
$elem.fadeIn = (val) => {
assert.equal(val, 1000);
faded_in = true;
};
common.copy_data_attribute_value(elem, "admin-emails");
common.copy_data_attribute_value($elem, "admin-emails");
assert.ok(removed);
assert.ok(faded_in);
assert.ok(faded_out);
@@ -111,17 +111,17 @@ run_test("adjust_mac_shortcuts mac", ({override_rewire}) => {
for (const [old_key, mac_key] of keys_to_test_mac) {
const test_item = {};
const stub = $.create("hotkey_" + key_no);
stub.text(old_key);
assert.equal(stub.hasClass("mac-cmd-key"), false);
test_item.stub = stub;
const $stub = $.create("hotkey_" + key_no);
$stub.text(old_key);
assert.equal($stub.hasClass("mac-cmd-key"), false);
test_item.$stub = $stub;
test_item.mac_key = mac_key;
test_item.is_cmd_key = old_key.includes("Ctrl");
test_items.push(test_item);
key_no += 1;
}
const children = test_items.map((test_item) => ({to_$: () => test_item.stub}));
const children = test_items.map((test_item) => ({to_$: () => test_item.$stub}));
$.create(".markdown_content", {children});
@@ -129,8 +129,8 @@ run_test("adjust_mac_shortcuts mac", ({override_rewire}) => {
common.adjust_mac_shortcuts(".markdown_content", require_cmd);
for (const test_item of test_items) {
assert.equal(test_item.stub.hasClass("mac-cmd-key"), test_item.is_cmd_key);
assert.equal(test_item.stub.text(), test_item.mac_key);
assert.equal(test_item.$stub.hasClass("mac-cmd-key"), test_item.is_cmd_key);
assert.equal(test_item.$stub.text(), test_item.mac_key);
}
});

View File

@@ -10,35 +10,35 @@ const blueslip = require("../zjsunit/zblueslip");
let env;
function make_tab(i) {
const self = {};
const $self = {};
assert.equal(env.tabs.length, i);
self.stub = true;
self.class = [];
$self.stub = true;
$self.class = [];
self.addClass = (c) => {
self.class += " " + c;
const tokens = self.class.trim().split(/ +/);
self.class = Array.from(new Set(tokens)).join(" ");
$self.addClass = (c) => {
$self.class += " " + c;
const tokens = $self.class.trim().split(/ +/);
$self.class = Array.from(new Set(tokens)).join(" ");
};
self.removeClass = (c) => {
const tokens = self.class.trim().split(/ +/);
self.class = tokens.filter((token) => token !== c).join(" ");
$self.removeClass = (c) => {
const tokens = $self.class.trim().split(/ +/);
$self.class = tokens.filter((token) => token !== c).join(" ");
};
self.hasClass = (c) => {
const tokens = self.class.trim().split(/ +/);
$self.hasClass = (c) => {
const tokens = $self.class.trim().split(/ +/);
return tokens.includes(c);
};
self.data = (name) => {
$self.data = (name) => {
assert.equal(name, "tab-id");
return i;
};
self.text = (text) => {
$self.text = (text) => {
assert.equal(
text,
[
@@ -49,23 +49,23 @@ function make_tab(i) {
);
};
self.trigger = (type) => {
$self.trigger = (type) => {
if (type === "focus") {
env.focused_tab = i;
}
};
env.tabs.push(self);
env.tabs.push($self);
return self;
return $self;
}
const ind_tab = (function () {
const self = {};
const $self = {};
self.stub = true;
$self.stub = true;
self.on = (name, f) => {
$self.on = (name, f) => {
if (name === "click") {
env.click_f = f;
} else if (name === "keydown") {
@@ -73,36 +73,36 @@ const ind_tab = (function () {
}
};
self.removeClass = (c) => {
for (const tab of env.tabs) {
tab.removeClass(c);
$self.removeClass = (c) => {
for (const $tab of env.tabs) {
$tab.removeClass(c);
}
};
self.eq = (idx) => env.tabs[idx];
$self.eq = (idx) => env.tabs[idx];
return self;
return $self;
})();
function make_switcher() {
const self = {};
const $self = {};
self.stub = true;
$self.stub = true;
self.children = [];
$self.children = [];
self.classList = new Set();
$self.classList = new Set();
self.append = (child) => {
self.children.push(child);
$self.append = (child) => {
$self.children.push(child);
};
self.addClass = (c) => {
self.classList.add(c);
self.addedClass = c;
$self.addClass = (c) => {
$self.classList.add(c);
$self.addedClass = c;
};
self.find = (sel) => {
$self.find = (sel) => {
switch (sel) {
case ".ind-tab":
return ind_tab;
@@ -111,7 +111,7 @@ function make_switcher() {
}
};
return self;
return $self;
}
mock_jquery((sel, attributes) => {
@@ -121,11 +121,10 @@ mock_jquery((sel, attributes) => {
}
switch (sel) {
case "<div class='tab-switcher'></div>":
return env.switcher;
case "<div class='tab-switcher stream_sorter_toggle'></div>":
return env.switcher;
case "<div>": {
if (attributes.class === "tab-switcher") {
return env.switcher;
}
const tab_id = attributes["data-tab-id"];
assert.deepEqual(
attributes,

View File

@@ -285,8 +285,8 @@ test_ui("enter_with_preview_open", ({override, override_rewire}) => {
override(reminder, "is_deferred_delivery", () => false);
override(document, "to_$", () => $("document-stub"));
let show_button_spinner_called = false;
override(loading, "show_button_spinner", (spinner) => {
assert.equal(spinner.selector, "#compose-send-button .loader");
override(loading, "show_button_spinner", ($spinner) => {
assert.equal($spinner.selector, "#compose-send-button .loader");
show_button_spinner_called = true;
});
@@ -333,8 +333,8 @@ test_ui("finish", ({override, override_rewire}) => {
override(reminder, "is_deferred_delivery", () => false);
override(document, "to_$", () => $("document-stub"));
let show_button_spinner_called = false;
override(loading, "show_button_spinner", (spinner) => {
assert.equal(spinner.selector, "#compose-send-button .loader");
override(loading, "show_button_spinner", ($spinner) => {
assert.equal($spinner.selector, "#compose-send-button .loader");
show_button_spinner_called = true;
});
@@ -552,27 +552,28 @@ test_ui("on_events", ({override, override_rewire}) => {
override(rendered_markdown, "update_elements", () => {});
function setup_parents_and_mock_remove(container_sel, target_sel, parent) {
const container = $.create("fake " + container_sel);
const $container = $.create("fake " + container_sel);
let container_removed = false;
container.remove = () => {
$container.remove = () => {
container_removed = true;
};
const target = $.create("fake click target (" + target_sel + ")");
const $target = $.create("fake click target (" + target_sel + ")");
target.set_parents_result(parent, container);
$target.set_parents_result(parent, $container);
const event = {
preventDefault: noop,
stopPropagation: noop,
target,
// FIXME: event.target should not be a jQuery object
target: $target,
};
const helper = {
event,
container,
target,
$container,
$target,
container_was_removed: () => container_removed,
};
@@ -633,7 +634,7 @@ test_ui("on_events", ({override, override_rewire}) => {
".compose_invite_user",
);
helper.container.data = (field) => {
helper.$container.data = (field) => {
if (field === "user-id") {
return "34";
}
@@ -642,7 +643,7 @@ test_ui("on_events", ({override, override_rewire}) => {
}
throw new Error(`Unknown field ${field}`);
};
helper.target.prop("disabled", false);
helper.$target.prop("disabled", false);
// !sub will result in true here and we check the success code path.
stream_data.add_sub(subscription);
@@ -814,8 +815,8 @@ test_ui("on_events", ({override, override_rewire}) => {
function test(func, param) {
let destroy_indicator_called = false;
override(loading, "destroy_indicator", (spinner) => {
assert.equal(spinner, $("#compose .markdown_preview_spinner"));
override(loading, "destroy_indicator", ($spinner) => {
assert.equal($spinner, $("#compose .markdown_preview_spinner"));
destroy_indicator_called = true;
});
setup_mock_markdown_contains_backend_only_syntax(current_message, true);
@@ -852,8 +853,8 @@ test_ui("on_events", ({override, override_rewire}) => {
setup_mock_markdown_contains_backend_only_syntax("```foobarfoobar```", true);
setup_mock_markdown_is_status_message("```foobarfoobar```", false);
override(loading, "make_indicator", (spinner) => {
assert.equal(spinner.selector, "#compose .markdown_preview_spinner");
override(loading, "make_indicator", ($spinner) => {
assert.equal($spinner.selector, "#compose .markdown_preview_spinner");
make_indicator_called = true;
});

View File

@@ -43,9 +43,9 @@ run_test("pills", ({override}) => {
people.get_realm_users = () => [iago, othello, hamlet];
const recipient_stub = $("#private_message_recipient");
const $recipient_stub = $("#private_message_recipient");
const pill_container_stub = "pill-container";
recipient_stub.set_parent(pill_container_stub);
$recipient_stub.set_parent(pill_container_stub);
let create_item_handler;
const all_pills = new Map();
@@ -132,7 +132,7 @@ run_test("pills", ({override}) => {
}
function input_pill_stub(opts) {
assert.equal(opts.container, pill_container_stub);
assert.equal(opts.$container, pill_container_stub);
create_item_handler = opts.create_item_from_text;
assert.ok(create_item_handler);
return pills;

View File

@@ -46,48 +46,48 @@ people.add_active_user(bob);
function make_textbox(s) {
// Simulate a jQuery textbox for testing purposes.
const widget = {};
const $widget = {};
widget.s = s;
widget.focused = false;
$widget.s = s;
$widget.focused = false;
widget.caret = function (arg) {
$widget.caret = function (arg) {
if (typeof arg === "number") {
widget.pos = arg;
$widget.pos = arg;
return this;
}
if (arg) {
widget.insert_pos = widget.pos;
widget.insert_text = arg;
const before = widget.s.slice(0, widget.pos);
const after = widget.s.slice(widget.pos);
widget.s = before + arg + after;
widget.pos += arg.length;
$widget.insert_pos = $widget.pos;
$widget.insert_text = arg;
const before = $widget.s.slice(0, $widget.pos);
const after = $widget.s.slice($widget.pos);
$widget.s = before + arg + after;
$widget.pos += arg.length;
return this;
}
return widget.pos;
return $widget.pos;
};
widget.val = function (new_val) {
$widget.val = function (new_val) {
if (new_val) {
widget.s = new_val;
$widget.s = new_val;
return this;
}
return widget.s;
return $widget.s;
};
widget.trigger = function (type) {
$widget.trigger = function (type) {
if (type === "focus") {
widget.focused = true;
$widget.focused = true;
} else if (type === "blur") {
widget.focused = false;
$widget.focused = false;
}
return this;
};
return widget;
return $widget;
}
run_test("autosize_textarea", ({override}) => {
@@ -121,59 +121,59 @@ run_test("insert_syntax_and_focus", () => {
});
run_test("smart_insert", () => {
let textbox = make_textbox("abc");
textbox.caret(4);
let $textbox = make_textbox("abc");
$textbox.caret(4);
compose_ui.smart_insert(textbox, ":smile:");
assert.equal(textbox.insert_pos, 4);
assert.equal(textbox.insert_text, " :smile: ");
assert.equal(textbox.val(), "abc :smile: ");
assert.ok(textbox.focused);
compose_ui.smart_insert($textbox, ":smile:");
assert.equal($textbox.insert_pos, 4);
assert.equal($textbox.insert_text, " :smile: ");
assert.equal($textbox.val(), "abc :smile: ");
assert.ok($textbox.focused);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, ":airplane:");
assert.equal(textbox.insert_text, ":airplane: ");
assert.equal(textbox.val(), "abc :smile: :airplane: ");
assert.ok(textbox.focused);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, ":airplane:");
assert.equal($textbox.insert_text, ":airplane: ");
assert.equal($textbox.val(), "abc :smile: :airplane: ");
assert.ok($textbox.focused);
textbox.caret(0);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, ":octopus:");
assert.equal(textbox.insert_text, ":octopus: ");
assert.equal(textbox.val(), ":octopus: abc :smile: :airplane: ");
assert.ok(textbox.focused);
$textbox.caret(0);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, ":octopus:");
assert.equal($textbox.insert_text, ":octopus: ");
assert.equal($textbox.val(), ":octopus: abc :smile: :airplane: ");
assert.ok($textbox.focused);
textbox.caret(textbox.val().length);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, ":heart:");
assert.equal(textbox.insert_text, ":heart: ");
assert.equal(textbox.val(), ":octopus: abc :smile: :airplane: :heart: ");
assert.ok(textbox.focused);
$textbox.caret($textbox.val().length);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, ":heart:");
assert.equal($textbox.insert_text, ":heart: ");
assert.equal($textbox.val(), ":octopus: abc :smile: :airplane: :heart: ");
assert.ok($textbox.focused);
// Test handling of spaces for ```quote
textbox = make_textbox("");
textbox.caret(0);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, "```quote\nquoted message\n```\n");
assert.equal(textbox.insert_text, "```quote\nquoted message\n```\n");
assert.equal(textbox.val(), "```quote\nquoted message\n```\n");
assert.ok(textbox.focused);
$textbox = make_textbox("");
$textbox.caret(0);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, "```quote\nquoted message\n```\n");
assert.equal($textbox.insert_text, "```quote\nquoted message\n```\n");
assert.equal($textbox.val(), "```quote\nquoted message\n```\n");
assert.ok($textbox.focused);
textbox = make_textbox("");
textbox.caret(0);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, "translated: [Quoting…]\n");
assert.equal(textbox.insert_text, "translated: [Quoting…]\n");
assert.equal(textbox.val(), "translated: [Quoting…]\n");
assert.ok(textbox.focused);
$textbox = make_textbox("");
$textbox.caret(0);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, "translated: [Quoting…]\n");
assert.equal($textbox.insert_text, "translated: [Quoting…]\n");
assert.equal($textbox.val(), "translated: [Quoting…]\n");
assert.ok($textbox.focused);
textbox = make_textbox("abc");
textbox.caret(3);
textbox.trigger("blur");
compose_ui.smart_insert(textbox, " test with space");
assert.equal(textbox.insert_text, " test with space ");
assert.equal(textbox.val(), "abc test with space ");
assert.ok(textbox.focused);
$textbox = make_textbox("abc");
$textbox.caret(3);
$textbox.trigger("blur");
compose_ui.smart_insert($textbox, " test with space");
assert.equal($textbox.insert_text, " test with space ");
assert.equal($textbox.val(), "abc test with space ");
assert.ok($textbox.focused);
// Note that we don't have any special logic for strings that are
// already surrounded by spaces, since we are usually inserting things
@@ -481,14 +481,14 @@ run_test("format_text", () => {
wrap_syntax = "";
}
const textarea = $("#compose-textarea");
textarea.get = () => ({
const $textarea = $("#compose-textarea");
$textarea.get = () => ({
setSelectionRange: () => {},
});
function init_textarea(val, range) {
textarea.val = () => val;
textarea.range = () => range;
$textarea.val = () => val;
$textarea.range = () => range;
}
const italic_syntax = "*";
@@ -502,7 +502,7 @@ run_test("format_text", () => {
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "bold");
compose_ui.format_text($textarea, "bold");
assert.equal(set_text, "");
assert.equal(wrap_selection_called, true);
assert.equal(wrap_syntax, bold_syntax);
@@ -515,7 +515,7 @@ run_test("format_text", () => {
text: "abc",
length: 7,
});
compose_ui.format_text(textarea, "bold");
compose_ui.format_text($textarea, "bold");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
@@ -527,7 +527,7 @@ run_test("format_text", () => {
text: "**abc**",
length: 7,
});
compose_ui.format_text(textarea, "bold");
compose_ui.format_text($textarea, "bold");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
@@ -539,7 +539,7 @@ run_test("format_text", () => {
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
compose_ui.format_text($textarea, "italic");
assert.equal(set_text, "");
assert.equal(wrap_selection_called, true);
assert.equal(wrap_syntax, italic_syntax);
@@ -552,7 +552,7 @@ run_test("format_text", () => {
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
compose_ui.format_text($textarea, "italic");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
@@ -564,7 +564,7 @@ run_test("format_text", () => {
text: "*abc*",
length: 5,
});
compose_ui.format_text(textarea, "italic");
compose_ui.format_text($textarea, "italic");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
@@ -576,7 +576,7 @@ run_test("format_text", () => {
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "bold");
compose_ui.format_text($textarea, "bold");
assert.equal(set_text, "*abc*");
assert.equal(wrap_selection_called, false);
@@ -588,7 +588,7 @@ run_test("format_text", () => {
text: "***abc***",
length: 9,
});
compose_ui.format_text(textarea, "bold");
compose_ui.format_text($textarea, "bold");
assert.equal(set_text, "*abc*");
assert.equal(wrap_selection_called, false);
@@ -600,7 +600,7 @@ run_test("format_text", () => {
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
compose_ui.format_text($textarea, "italic");
assert.equal(set_text, "**abc**");
assert.equal(wrap_selection_called, false);
@@ -612,14 +612,14 @@ run_test("format_text", () => {
text: "***abc***",
length: 9,
});
compose_ui.format_text(textarea, "italic");
compose_ui.format_text($textarea, "italic");
assert.equal(set_text, "**abc**");
assert.equal(wrap_selection_called, false);
});
run_test("markdown_shortcuts", ({override_rewire}) => {
let format_text_type;
override_rewire(compose_ui, "format_text", (textarea, type) => {
override_rewire(compose_ui, "format_text", ($textarea, type) => {
format_text_type = type;
});
@@ -711,21 +711,21 @@ run_test("markdown_shortcuts", ({override_rewire}) => {
});
run_test("right-to-left", () => {
const textarea = $("#compose-textarea");
const $textarea = $("#compose-textarea");
const event = {
key: "A",
};
assert.equal(textarea.hasClass("rtl"), false);
assert.equal($textarea.hasClass("rtl"), false);
textarea.val("```quote\nمرحبا");
$textarea.val("```quote\nمرحبا");
compose_ui.handle_keyup(event, $("#compose-textarea"));
assert.equal(textarea.hasClass("rtl"), true);
assert.equal($textarea.hasClass("rtl"), true);
textarea.val("```quote foo");
compose_ui.handle_keyup(event, textarea);
$textarea.val("```quote foo");
compose_ui.handle_keyup(event, $textarea);
assert.equal(textarea.hasClass("rtl"), false);
assert.equal($textarea.hasClass("rtl"), false);
});

View File

@@ -131,10 +131,10 @@ test_ui("validate", ({override, mock_template}) => {
$("#compose-send-button").trigger("focus");
$("#compose-send-button .loader").hide();
const pm_pill_container = $.create("fake-pm-pill-container");
const $pm_pill_container = $.create("fake-pm-pill-container");
$("#private_message_recipient")[0] = {};
$("#private_message_recipient").set_parent(pm_pill_container);
pm_pill_container.set_find_results(".input", $("#private_message_recipient"));
$("#private_message_recipient").set_parent($pm_pill_container);
$pm_pill_container.set_find_results(".input", $("#private_message_recipient"));
$("#private_message_recipient").before = () => {};
compose_pm_pill.initialize();
@@ -474,38 +474,38 @@ test_ui("test_validate_stream_message_post_policy_full_members_only", () => {
test_ui("test_check_overflow_text", () => {
page_params.max_message_length = 10000;
const textarea = $("#compose-textarea");
const indicator = $("#compose_limit_indicator");
const send_button = $("#compose-send-button");
const $textarea = $("#compose-textarea");
const $indicator = $("#compose_limit_indicator");
const $send_button = $("#compose-send-button");
// Indicator should show red colored text
textarea.val("a".repeat(10000 + 1));
$textarea.val("a".repeat(10000 + 1));
compose_validate.check_overflow_text();
assert.ok(indicator.hasClass("over_limit"));
assert.equal(indicator.text(), "10001/10000");
assert.ok(textarea.hasClass("over_limit"));
assert.ok($indicator.hasClass("over_limit"));
assert.equal($indicator.text(), "10001/10000");
assert.ok($textarea.hasClass("over_limit"));
assert.equal(
$("#compose-error-msg").html(),
"translated HTML: Message length shouldn't be greater than 10000 characters.",
);
assert.ok(send_button.prop("disabled"));
assert.ok($send_button.prop("disabled"));
$("#compose-send-status").stop = () => ({fadeOut: () => {}});
// Indicator should show orange colored text
textarea.val("a".repeat(9000 + 1));
$textarea.val("a".repeat(9000 + 1));
compose_validate.check_overflow_text();
assert.ok(!indicator.hasClass("over_limit"));
assert.equal(indicator.text(), "9001/10000");
assert.ok(!textarea.hasClass("over_limit"));
assert.ok(!send_button.prop("disabled"));
assert.ok(!$indicator.hasClass("over_limit"));
assert.equal($indicator.text(), "9001/10000");
assert.ok(!$textarea.hasClass("over_limit"));
assert.ok(!$send_button.prop("disabled"));
// Indicator must be empty
textarea.val("a".repeat(9000));
$textarea.val("a".repeat(9000));
compose_validate.check_overflow_text();
assert.ok(!indicator.hasClass("over_limit"));
assert.equal(indicator.text(), "");
assert.ok(!textarea.hasClass("over_limit"));
assert.ok(!$indicator.hasClass("over_limit"));
assert.equal($indicator.text(), "");
assert.ok(!$textarea.hasClass("over_limit"));
});
test_ui("test_message_overflow", () => {
@@ -736,10 +736,10 @@ test_ui("warn_if_mentioning_unsubscribed_user", ({override, override_rewire, moc
}
// Simulate that the row was added to the DOM.
const warning_row = $("<warning row>");
const $warning_row = $("<warning-row-stub>");
let looked_for_existing;
warning_row.data = (field) => {
$warning_row.data = (field) => {
if (field === "user-id") {
looked_for_existing = true;
return "34";
@@ -750,9 +750,9 @@ test_ui("warn_if_mentioning_unsubscribed_user", ({override, override_rewire, moc
throw new Error(`Unknown field ${field}`);
};
const previous_users = $("#compose_invite_users .compose_invite_user");
previous_users.length = 1;
previous_users[0] = warning_row;
const $previous_users = $("#compose_invite_users .compose_invite_user");
$previous_users.length = 1;
$previous_users[0] = $warning_row;
$("#compose_invite_users").hide();
// Now try to mention the same person again. The template should
@@ -788,12 +788,12 @@ test_ui("test warn_if_topic_resolved", ({override, mock_template}) => {
stream_data.add_sub(sub);
// The error message area where it is shown
const error_area = $("#compose_resolved_topic");
const $error_area = $("#compose_resolved_topic");
compose_validate.clear_topic_resolved_warning();
// Hack to make this empty for zjquery; this is conceptually done
// in the previous line.
error_area.html("");
assert.ok(!error_area.visible());
$error_area.html("");
assert.ok(!$error_area.visible());
compose_state.set_message_type("stream");
compose_state.stream_name("Do not exist");
@@ -802,25 +802,25 @@ test_ui("test warn_if_topic_resolved", ({override, mock_template}) => {
// Do not show a warning if stream name does not exist
compose_validate.warn_if_topic_resolved(true);
assert.ok(!error_area.visible());
assert.ok(!$error_area.visible());
compose_state.stream_name("random");
// Show the warning now as stream also exists
compose_validate.warn_if_topic_resolved(true);
assert.ok(error_area.visible());
assert.ok($error_area.visible());
// Call it again with false; this should be a noop.
compose_validate.warn_if_topic_resolved(false);
assert.ok(error_area.visible());
assert.ok($error_area.visible());
compose_state.topic("hello");
// The warning will be cleared now
compose_validate.warn_if_topic_resolved(true);
assert.ok(!error_area.visible());
assert.ok(!$error_area.visible());
// Calling with false won't do anything.
compose_validate.warn_if_topic_resolved(false);
assert.ok(!error_area.visible());
assert.ok(!$error_area.visible());
});

View File

@@ -27,14 +27,15 @@ set_global("ResizeObserver", function () {
const server_events_dispatch = zrequire("server_events_dispatch");
const compose_ui = zrequire("compose_ui");
const compose_closed = zrequire("compose_closed_ui");
const compose = zrequire("compose");
function stub_out_video_calls() {
const elem = $("#below-compose-content .video_link");
elem.toggle = (show) => {
const $elem = $("#below-compose-content .video_link");
$elem.toggle = (show) => {
if (show) {
elem.show();
$elem.show();
} else {
elem.hide();
$elem.hide();
}
};
}
@@ -78,14 +79,14 @@ test("videos", ({override, override_rewire}) => {
(function test_no_provider_video_link_compose_clicked() {
let called = false;
const textarea = $.create("target-stub");
textarea.set_parents_result(".message_edit_form", []);
const $textarea = $.create("target-stub");
$textarea.set_parents_result(".message_edit_form", []);
const ev = {
preventDefault: () => {},
stopPropagation: () => {},
target: {
to_$: () => textarea,
to_$: () => $textarea,
},
};
@@ -104,14 +105,14 @@ test("videos", ({override, override_rewire}) => {
let syntax_to_insert;
let called = false;
const textarea = $.create("jitsi-target-stub");
textarea.set_parents_result(".message_edit_form", []);
const $textarea = $.create("jitsi-target-stub");
$textarea.set_parents_result(".message_edit_form", []);
const ev = {
preventDefault: () => {},
stopPropagation: () => {},
target: {
to_$: () => textarea,
to_$: () => $textarea,
},
};
@@ -142,14 +143,14 @@ test("videos", ({override, override_rewire}) => {
let syntax_to_insert;
let called = false;
const textarea = $.create("zoom-target-stub");
textarea.set_parents_result(".message_edit_form", []);
const $textarea = $.create("zoom-target-stub");
$textarea.set_parents_result(".message_edit_form", []);
const ev = {
preventDefault: () => {},
stopPropagation: () => {},
target: {
to_$: () => textarea,
to_$: () => $textarea,
},
};
@@ -188,14 +189,14 @@ test("videos", ({override, override_rewire}) => {
let syntax_to_insert;
let called = false;
const textarea = $.create("bbb-target-stub");
textarea.set_parents_result(".message_edit_form", []);
const $textarea = $.create("bbb-target-stub");
$textarea.set_parents_result(".message_edit_form", []);
const ev = {
preventDefault: () => {},
stopPropagation: () => {},
target: {
to_$: () => textarea,
to_$: () => $textarea,
},
};
@@ -210,8 +211,11 @@ test("videos", ({override, override_rewire}) => {
page_params.realm_video_chat_provider =
realm_available_video_chat_providers.big_blue_button.id;
compose_closed.get_recipient_label = () => "a";
channel.get = (options) => {
assert.equal(options.url, "/json/calls/bigbluebutton/create");
assert.equal(options.data.meeting_name, "a meeting");
options.success({
url: "/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22AAAAAAAAAA%22&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22",
});

View File

@@ -498,10 +498,20 @@ run_test("realm_bot remove", ({override}) => {
admin_stub.get_args("update_user_id", "update_bot_data");
});
run_test("realm_bot delete", () => {
run_test("realm_bot delete", ({override}) => {
const event = event_fixtures.realm_bot__delete;
// We don't handle live updates for delete events, this is a noop.
const bot_stub = make_stub();
const admin_stub = make_stub();
override(bot_data, "del", bot_stub.f);
override(settings_bots, "render_bots", () => {});
override(settings_users, "redraw_bots_list", admin_stub.f);
dispatch(event);
assert.equal(bot_stub.num_calls, 1);
const args = bot_stub.get_args("user_id");
assert_same(args.user_id, event.bot.user_id);
assert.equal(admin_stub.num_calls, 1);
});
run_test("realm_bot update", ({override}) => {

View File

@@ -120,8 +120,8 @@ test("draft_model add", ({override}) => {
const ls = localstorage();
assert.equal(ls.get("draft"), undefined);
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
override(Date, "now", () => 1);
const expected = {...draft_1};
@@ -136,8 +136,8 @@ test("draft_model edit", () => {
assert.equal(ls.get("draft"), undefined);
let id;
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
with_overrides(({override}) => {
override(Date, "now", () => 1);
@@ -161,8 +161,8 @@ test("draft_model delete", ({override}) => {
const ls = localstorage();
assert.equal(ls.get("draft"), undefined);
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
override(Date, "now", () => 1);
const expected = {...draft_1};
@@ -212,8 +212,8 @@ test("initialize", ({override_rewire}) => {
assert.ok(called);
};
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
drafts.initialize();
});
@@ -239,8 +239,8 @@ test("remove_old_drafts", () => {
ls.set("drafts", data);
assert.deepEqual(draft_model.get(), data);
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
drafts.remove_old_drafts();
assert.deepEqual(draft_model.get(), {id3: draft_3});
@@ -256,9 +256,9 @@ test("update_draft", ({override}) => {
override(compose_state, "get_message_type", () => "private");
override(compose_state, "private_message_recipient", () => "aaron@zulip.com");
const container = $(".top_left_drafts");
const child = $(".unread_count");
container.set_find_results(".unread_count", child);
const $container = $(".top_left_drafts");
const $child = $(".unread_count");
$container.set_find_results(".unread_count", $child);
tippy_args = {
content: "translated: Saved as draft",
@@ -313,8 +313,8 @@ test("delete_all_drafts", () => {
ls.set("drafts", data);
assert.deepEqual(draft_model.get(), data);
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
drafts.delete_all_drafts();
assert.deepEqual(draft_model.get(), {});
@@ -451,8 +451,8 @@ test("format_drafts", ({override_rewire, mock_template}) => {
expected[0].stream_name = "stream-rename";
const unread_count = $('<span class="unread_count"></span>');
$(".top_left_drafts").set_find_results(".unread_count", unread_count);
const $unread_count = $("<unread-count-stub>");
$(".top_left_drafts").set_find_results(".unread_count", $unread_count);
drafts.launch();
timerender.__Rewire__("render_now", stub_render_now);

View File

@@ -25,12 +25,12 @@ const {DropdownListWidget, MultiSelectDropdownListWidget} = zrequire("dropdown_l
// For DropdownListWidget
const setup_dropdown_zjquery_data = (name) => {
const input_group = $(".input_group");
const reset_button = $(".dropdown_list_reset_button");
input_group.set_find_results(".dropdown_list_reset_button", reset_button);
$(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`).closest = () => input_group;
const $input_group = $(".input_group");
const $reset_button = $(".dropdown_list_reset_button");
$input_group.set_find_results(".dropdown_list_reset_button", $reset_button);
$(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`).closest = () => $input_group;
const $widget = $(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`);
return {reset_button, $widget};
return {$reset_button, $widget};
};
run_test("basic_functions", () => {
@@ -46,31 +46,31 @@ run_test("basic_functions", () => {
render_text: (text) => `rendered: ${text}`,
};
const {reset_button, $widget} = setup_dropdown_zjquery_data(opts.widget_name);
const {$reset_button, $widget} = setup_dropdown_zjquery_data(opts.widget_name);
const widget = new DropdownListWidget(opts);
assert.equal(widget.value(), "one");
assert.equal(updated_value, undefined); // We haven't 'updated' the widget yet.
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
widget.update("two");
assert.equal($widget.text(), "rendered: two");
assert.equal(widget.value(), "two");
assert.equal(updated_value, "two");
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
widget.update(null);
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), "");
assert.equal(updated_value, null);
assert.ok(!reset_button.visible());
assert.ok(!$reset_button.visible());
widget.update("four");
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), "four");
assert.equal(updated_value, "four");
assert.ok(!reset_button.visible());
assert.ok(!$reset_button.visible());
});
run_test("no_default_value", () => {
@@ -110,7 +110,7 @@ run_test("basic MDLW functions", () => {
default_text: $t({defaultMessage: "not set"}),
};
const {reset_button, $widget} = setup_multiselect_dropdown_zjquery_data(opts.widget_name);
const {$reset_button, $widget} = setup_multiselect_dropdown_zjquery_data(opts.widget_name);
const widget = new MultiSelectDropdownListWidget(opts);
function set_dropdown_variables(widget, value) {
@@ -121,7 +121,7 @@ run_test("basic MDLW functions", () => {
assert.deepEqual(widget.value(), ["one"]);
assert.equal(updated_value, undefined);
assert.equal($widget.text(), "one");
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
set_dropdown_variables(widget, ["one", "two"]);
widget.update(widget.data_selected);
@@ -129,7 +129,7 @@ run_test("basic MDLW functions", () => {
assert.equal($widget.text(), "one,two");
assert.deepEqual(widget.value(), ["one", "two"]);
assert.deepEqual(updated_value, ["one", "two"]);
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
set_dropdown_variables(widget, ["one", "two", "three"]);
widget.update(widget.data_selected);
@@ -137,7 +137,7 @@ run_test("basic MDLW functions", () => {
assert.equal($widget.text(), "translated: 3 selected");
assert.deepEqual(widget.value(), ["one", "two", "three"]);
assert.deepEqual(updated_value, ["one", "two", "three"]);
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
set_dropdown_variables(widget, null);
widget.update(widget.data_selected);
@@ -145,7 +145,7 @@ run_test("basic MDLW functions", () => {
assert.equal($widget.text(), "translated: not set");
assert.equal(widget.value(), null);
assert.equal(updated_value, null);
assert.ok(!reset_button.visible());
assert.ok(!$reset_button.visible());
set_dropdown_variables(widget, ["one"]);
widget.update(widget.data_selected);
@@ -153,7 +153,7 @@ run_test("basic MDLW functions", () => {
assert.equal($widget.text(), "one");
assert.deepEqual(widget.value(), ["one"]);
assert.deepEqual(updated_value, ["one"]);
assert.ok(reset_button.visible());
assert.ok($reset_button.visible());
});
run_test("MDLW no_default_value", () => {

View File

@@ -29,6 +29,8 @@ const message_store = mock_esm("../../static/js/message_store", {
get: () => ({failed_request: true}),
update_booleans: () => {},
set_message_booleans: () => {},
});
mock_esm("../../static/js/message_list");

View File

@@ -64,14 +64,13 @@ run_test("message_store", () => {
const in_message = {...messages.isaac_to_denmark_stream};
assert.equal(in_message.alerted, undefined);
message_store.set_message_booleans(in_message);
assert.equal(in_message.alerted, true);
// Let's add a message into our message_store via
// message_helper.process_new_message.
assert.equal(message_store.get(in_message.id), undefined);
message_helper.process_new_message(in_message);
const message = message_store.get(in_message.id);
assert.equal(in_message.alerted, true);
assert.equal(message, in_message);
// There are more side effects.

View File

@@ -67,7 +67,7 @@ people.add_active_user(kitty);
*/
run_test("typing_events.render_notifications_for_narrow", ({override_rewire, mock_template}) => {
// All typists are rendered in `#typing_notifications`.
const typing_notifications = $("#typing_notifications");
const $typing_notifications = $("#typing_notifications");
const two_typing_users_ids = [anna.user_id, vronsky.user_id];
const two_typing_users = [anna, vronsky];
@@ -107,7 +107,7 @@ run_test("typing_events.render_notifications_for_narrow", ({override_rewire, moc
typing_events.render_notifications_for_narrow();
// Make sure #typing_notifications's html content is set to the rendered template
// which we mocked and gave a custom return value.
assert.equal(typing_notifications.html(), two_typing_users_rendered_html);
assert.equal($typing_notifications.html(), two_typing_users_rendered_html);
// Now we'll see how setting the second argument to `true`
// can be helpful in testing conditionals inside the template.
@@ -118,9 +118,9 @@ run_test("typing_events.render_notifications_for_narrow", ({override_rewire, moc
// Since we only have two(<MAX_USERS_TO_DISPLAY_NAME) typists, both of them
// should be rendered but not 'Several people are typing…'
typing_events.render_notifications_for_narrow();
assert.ok(typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes("Several people are typing…"));
assert.ok($typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok($typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes("Several people are typing…"));
// Change to having four typists and verify the rendered html has
// 'Several people are typing…' but not the list of users.
@@ -128,9 +128,9 @@ run_test("typing_events.render_notifications_for_narrow", ({override_rewire, moc
override_rewire(typing_events, "get_users_typing_for_narrow", () => four_typing_users_ids);
typing_events.render_notifications_for_narrow();
assert.ok(typing_notifications.html().includes("Several people are typing…"));
assert.ok(!typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${kitty.full_name} is typing…`));
assert.ok($typing_notifications.html().includes("Several people are typing…"));
assert.ok(!$typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${kitty.full_name} is typing…`));
});

View File

@@ -8,8 +8,8 @@ const blueslip = require("../zjsunit/zblueslip");
const $ = require("../zjsunit/zjquery");
const {user_settings} = require("../zjsunit/zpage_params");
let window_stub;
set_global("to_$", () => window_stub);
let $window_stub;
set_global("to_$", () => $window_stub);
mock_esm("../../static/js/search", {
update_button_visibility: () => {},
@@ -165,7 +165,7 @@ function test_helper({override, override_rewire, change_tab}) {
}
run_test("hash_interactions", ({override, override_rewire}) => {
window_stub = $.create("window-stub");
$window_stub = $.create("window-stub");
user_settings.default_view = "recent_topics";
override_rewire(recent_topics_util, "is_visible", () => false);
@@ -189,7 +189,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#all_messages";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[message_viewport, "stop_auto_scrolling"],
@@ -199,7 +199,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
]);
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[message_viewport, "stop_auto_scrolling"],
@@ -211,7 +211,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#narrow/stream/Denmark";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[message_viewport, "stop_auto_scrolling"],
@@ -225,7 +225,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#narrow";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[message_viewport, "stop_auto_scrolling"],
@@ -240,7 +240,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#narrow/foo.foo";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[message_viewport, "stop_auto_scrolling"],
@@ -251,7 +251,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#streams/whatever";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[stream_settings_ui, "launch"],
@@ -261,7 +261,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#reload:send_after_reload=0...";
helper.clear_events();
window_stub.trigger("hashchange");
$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);
@@ -269,25 +269,25 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#keyboard-shortcuts/whatever";
helper.clear_events();
window_stub.trigger("hashchange");
$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");
$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");
$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");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[drafts, "launch"],
@@ -296,7 +296,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#settings/alert-words";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[settings, "launch"],
@@ -305,7 +305,7 @@ run_test("hash_interactions", ({override, override_rewire}) => {
window.location.hash = "#organization/user-list-admin";
helper.clear_events();
window_stub.trigger("hashchange");
$window_stub.trigger("hashchange");
helper.assert_events([
[overlays, "close_for_hash_change"],
[admin, "launch"],

View File

@@ -69,12 +69,12 @@ run_test("basics", ({override_rewire, mock_template}) => {
blueslip.expect("error", "Pill needs container.");
input_pill.create(config);
const pill_input = $.create("pill_input");
const container = $.create("container");
container.set_find_results(".input", pill_input);
const $pill_input = $.create("pill_input");
const $container = $.create("container");
$container.set_find_results(".input", $pill_input);
blueslip.expect("error", "Pill needs create_item_from_text");
config.container = container;
config.$container = $container;
input_pill.create(config);
blueslip.expect("error", "Pill needs get_text_from_item");
@@ -101,9 +101,9 @@ run_test("basics", ({override_rewire, mock_template}) => {
let inserted_before;
const expected_html = pill_html("JavaScript", "some_id1", example_img_link, status_emoji_info);
pill_input.before = (elem) => {
$pill_input.before = ($elem) => {
inserted_before = true;
assert.equal(elem.html(), expected_html);
assert.equal($elem.html(), expected_html);
};
widget.appendValidatedData(item);
@@ -134,27 +134,27 @@ function set_up() {
},
};
const pill_input = $.create("pill_input");
const $pill_input = $.create("pill_input");
pill_input[0] = {};
pill_input.before = () => {};
$pill_input[0] = {};
$pill_input.before = () => {};
const create_item_from_text = (text) => items[text];
const container = $.create("container");
container.set_find_results(".input", pill_input);
const $container = $.create("container");
$container.set_find_results(".input", $pill_input);
const config = {
container,
$container,
create_item_from_text,
get_text_from_item: (item) => item.display_value,
};
return {
config,
pill_input,
$pill_input,
items,
container,
$container,
};
}
@@ -167,16 +167,16 @@ run_test("copy from pill", ({override_rewire, mock_template}) => {
override_random_id({override_rewire});
const info = set_up();
const config = info.config;
const container = info.container;
const $container = info.$container;
const widget = input_pill.create(config);
widget.appendValue("blue,red");
const copy_handler = container.get_on_handler("copy", ".pill");
const copy_handler = $container.get_on_handler("copy", ".pill");
let copied_text;
const pill_stub = {
const $pill_stub = {
data: (field) => {
assert.equal(field, "id");
return "some_id2";
@@ -195,7 +195,7 @@ run_test("copy from pill", ({override_rewire, mock_template}) => {
preventDefault: noop,
};
container.set_find_results(":focus", pill_stub);
$container.set_find_results(":focus", $pill_stub);
copy_handler(e);
@@ -210,12 +210,12 @@ run_test("paste to input", ({mock_template}) => {
const info = set_up();
const config = info.config;
const container = info.container;
const $container = info.$container;
const items = info.items;
const widget = input_pill.create(config);
const paste_handler = container.get_on_handler("paste", ".input");
const paste_handler = $container.get_on_handler("paste", ".input");
const paste_text = "blue,yellow";
@@ -233,7 +233,7 @@ run_test("paste to input", ({mock_template}) => {
document.execCommand = (cmd, _, text) => {
assert.equal(cmd, "insertText");
container.find(".input").text(text);
$container.find(".input").text(text);
};
paste_handler(e);
@@ -257,12 +257,12 @@ run_test("arrows on pills", ({mock_template}) => {
const info = set_up();
const config = info.config;
const container = info.container;
const $container = info.$container;
const widget = input_pill.create(config);
widget.appendValue("blue,red");
const key_handler = container.get_on_handler("keydown", ".pill");
const key_handler = $container.get_on_handler("keydown", ".pill");
function test_key(c) {
key_handler({
@@ -273,7 +273,7 @@ run_test("arrows on pills", ({mock_template}) => {
let prev_focused = false;
let next_focused = false;
const pill_stub = {
const $pill_stub = {
prev: () => ({
trigger: (type) => {
if (type === "focus") {
@@ -290,7 +290,7 @@ run_test("arrows on pills", ({mock_template}) => {
}),
};
container.set_find_results(".pill:focus", pill_stub);
$container.set_find_results(".pill:focus", $pill_stub);
// We use the same stub to test both arrows, since we don't
// actually cause any real state changes here. We stub out
@@ -310,16 +310,16 @@ run_test("left arrow on input", ({mock_template}) => {
const info = set_up();
const config = info.config;
const container = info.container;
const $container = info.$container;
const widget = input_pill.create(config);
widget.appendValue("blue,red");
const key_handler = container.get_on_handler("keydown", ".input");
const key_handler = $container.get_on_handler("keydown", ".input");
let last_pill_focused = false;
container.set_find_results(".pill", {
$container.set_find_results(".pill", {
last: () => ({
trigger: (type) => {
if (type === "focus") {
@@ -345,17 +345,17 @@ run_test("comma", ({mock_template}) => {
const info = set_up();
const config = info.config;
const items = info.items;
const pill_input = info.pill_input;
const container = info.container;
const $pill_input = info.$pill_input;
const $container = info.$container;
const widget = input_pill.create(config);
widget.appendValue("blue,red");
assert.deepEqual(widget.items(), [items.blue, items.red]);
const key_handler = container.get_on_handler("keydown", ".input");
const key_handler = $container.get_on_handler("keydown", ".input");
pill_input.text(" yel");
$pill_input.text(" yel");
key_handler({
key: ",",
@@ -364,7 +364,7 @@ run_test("comma", ({mock_template}) => {
assert.deepEqual(widget.items(), [items.blue, items.red]);
pill_input.text(" yellow");
$pill_input.text(" yellow");
key_handler({
key: ",",
@@ -383,14 +383,14 @@ run_test("Enter key with text", ({mock_template}) => {
const info = set_up();
const config = info.config;
const items = info.items;
const container = info.container;
const $container = info.$container;
const widget = input_pill.create(config);
widget.appendValue("blue,red");
assert.deepEqual(widget.items(), [items.blue, items.red]);
const key_handler = container.get_on_handler("keydown", ".input");
const key_handler = $container.get_on_handler("keydown", ".input");
key_handler({
key: "Enter",
@@ -415,13 +415,13 @@ run_test("insert_remove", ({override_rewire, mock_template}) => {
const info = set_up();
const config = info.config;
const pill_input = info.pill_input;
const $pill_input = info.$pill_input;
const items = info.items;
const container = info.container;
const $container = info.$container;
const inserted_html = [];
pill_input.before = (elem) => {
inserted_html.push(elem.html());
$pill_input.before = ($elem) => {
inserted_html.push($elem.html());
};
const widget = input_pill.create(config);
@@ -450,11 +450,11 @@ run_test("insert_remove", ({override_rewire, mock_template}) => {
assert.deepEqual(widget.items(), [items.blue, items.red, items.yellow]);
assert.equal(pill_input.text(), "chartreuse, mauve");
assert.equal($pill_input.text(), "chartreuse, mauve");
assert.equal(widget.is_pending(), true);
widget.clear_text();
assert.equal(pill_input.text(), "");
assert.equal($pill_input.text(), "");
assert.equal(widget.is_pending(), false);
let color_removed;
@@ -469,7 +469,7 @@ run_test("insert_remove", ({override_rewire, mock_template}) => {
pill.$element.remove = set_colored_removed_func(pill.item.display_value);
}
let key_handler = container.get_on_handler("keydown", ".input");
let key_handler = $container.get_on_handler("keydown", ".input");
key_handler({
key: "Backspace",
@@ -486,7 +486,7 @@ run_test("insert_remove", ({override_rewire, mock_template}) => {
let next_pill_focused = false;
const next_pill_stub = {
const $next_pill_stub = {
trigger: (type) => {
if (type === "focus") {
next_pill_focused = true;
@@ -494,17 +494,17 @@ run_test("insert_remove", ({override_rewire, mock_template}) => {
},
};
const focus_pill_stub = {
next: () => next_pill_stub,
const $focus_pill_stub = {
next: () => $next_pill_stub,
data: (field) => {
assert.equal(field, "id");
return "some_id1";
},
};
container.set_find_results(".pill:focus", focus_pill_stub);
$container.set_find_results(".pill:focus", $focus_pill_stub);
key_handler = container.get_on_handler("keydown", ".pill");
key_handler = $container.get_on_handler("keydown", ".pill");
key_handler({
key: "Backspace",
preventDefault: noop,
@@ -526,7 +526,7 @@ run_test("exit button on pill", ({override_rewire, mock_template}) => {
const config = info.config;
const items = info.items;
const container = info.container;
const $container = info.$container;
const widget = input_pill.create(config);
@@ -539,7 +539,7 @@ run_test("exit button on pill", ({override_rewire, mock_template}) => {
let next_pill_focused = false;
const next_pill_stub = {
const $next_pill_stub = {
trigger: (type) => {
if (type === "focus") {
next_pill_focused = true;
@@ -547,8 +547,8 @@ run_test("exit button on pill", ({override_rewire, mock_template}) => {
},
};
const curr_pill_stub = {
next: () => next_pill_stub,
const $curr_pill_stub = {
next: () => $next_pill_stub,
data: (field) => {
assert.equal(field, "id");
return "some_id1";
@@ -559,7 +559,7 @@ run_test("exit button on pill", ({override_rewire, mock_template}) => {
to_$: () => ({
closest: (sel) => {
assert.equal(sel, ".pill");
return curr_pill_stub;
return $curr_pill_stub;
},
}),
};
@@ -567,7 +567,7 @@ run_test("exit button on pill", ({override_rewire, mock_template}) => {
const e = {
stopPropagation: noop,
};
const exit_click_handler = container.get_on_handler("click", ".exit");
const exit_click_handler = $container.get_on_handler("click", ".exit");
exit_click_handler.call(exit_button_stub, e);
@@ -580,13 +580,13 @@ run_test("misc things", () => {
const info = set_up();
const config = info.config;
const container = info.container;
const pill_input = info.pill_input;
const $container = info.$container;
const $pill_input = info.$pill_input;
const widget = input_pill.create(config);
// animation
const animation_end_handler = container.get_on_handler("animationend", ".input");
const animation_end_handler = $container.get_on_handler("animationend", ".input");
let shake_class_removed = false;
@@ -614,17 +614,17 @@ run_test("misc things", () => {
});
// click on container
const container_click_handler = container.get_on_handler("click");
const container_click_handler = $container.get_on_handler("click");
const stub = $.create("the-pill-container");
stub.set_find_results(".input", pill_input);
stub.is = (sel) => {
const $stub = $.create("the-pill-container");
$stub.set_find_results(".input", $pill_input);
$stub.is = (sel) => {
assert.equal(sel, ".pill-container");
return true;
};
const this_ = {
to_$: () => stub,
to_$: () => $stub,
};
container_click_handler.call(this_, {target: this_});
@@ -637,18 +637,18 @@ run_test("appendValue/clear", ({mock_template}) => {
return html;
});
const pill_input = $.create("pill_input");
const container = $.create("container");
container.set_find_results(".input", pill_input);
const $pill_input = $.create("pill_input");
const $container = $.create("container");
$container.set_find_results(".input", $pill_input);
const config = {
container,
$container,
create_item_from_text: (s) => ({type: "color", display_value: s}),
get_text_from_item: (s) => s.display_value,
};
pill_input.before = () => {};
pill_input[0] = {};
$pill_input.before = () => {};
$pill_input[0] = {};
const widget = input_pill.create(config);
@@ -672,5 +672,5 @@ run_test("appendValue/clear", ({mock_template}) => {
// Note that we remove colors in the reverse order that we inserted.
assert.deepEqual(removed_colors, ["blue", "yellow", "red"]);
assert.equal(pill_input[0].textContent, "");
assert.equal($pill_input[0].textContent, "");
});

View File

@@ -7,9 +7,9 @@ const $ = require("../zjsunit/zjquery");
const keydown_util = zrequire("keydown_util");
run_test("test_early_returns", () => {
const stub = $.create("stub");
const $stub = $.create("stub");
const opts = {
elem: stub,
$elem: $stub,
handlers: {
ArrowLeft: () => {
throw new Error("do not dispatch this with alt key");
@@ -24,14 +24,14 @@ run_test("test_early_returns", () => {
key: "a", // not in keys
};
stub.trigger(e1);
$stub.trigger(e1);
const e2 = {
type: "keydown",
key: "Enter", // no handler
};
stub.trigger(e2);
$stub.trigger(e2);
const e3 = {
type: "keydown",
@@ -39,5 +39,5 @@ run_test("test_early_returns", () => {
altKey: true, // let browser handle
};
stub.trigger(e3);
$stub.trigger(e3);
});

View File

@@ -30,21 +30,21 @@ function test(label, f) {
}
test("pan_and_zoom", ({override_rewire}) => {
const img = $.create("img-stub");
const link = $.create("link-stub");
const msg = $.create("msg-stub");
const $img = $.create("img-stub");
const $link = $.create("link-stub");
const $msg = $.create("msg-stub");
$(img).closest = () => [];
$($img).closest = () => [];
img.set_parent(link);
link.closest = () => msg;
$img.set_parent($link);
$link.closest = () => $msg;
override_rewire(rows, "id", (row) => {
assert.equal(row, msg);
override_rewire(rows, "id", ($row) => {
assert.equal($row, $msg);
return 1234;
});
img.attr("src", "example");
$img.attr("src", "example");
let fetched_zid;
@@ -55,25 +55,25 @@ test("pan_and_zoom", ({override_rewire}) => {
override_rewire(lightbox, "render_lightbox_list_images", () => {});
const open_image = lightbox.build_open_image_function();
open_image(img);
open_image($img);
assert.equal(fetched_zid, 1234);
});
test("youtube", ({override_rewire}) => {
const href = "https://youtube.com/some-random-clip";
const img = $.create("img-stub");
const link = $.create("link-stub");
const msg = $.create("msg-stub");
const $img = $.create("img-stub");
const $link = $.create("link-stub");
const $msg = $.create("msg-stub");
override_rewire(rows, "id", (row) => {
assert.equal(row, msg);
override_rewire(rows, "id", ($row) => {
assert.equal($row, $msg);
return 4321;
});
$(img).attr("src", href);
$($img).attr("src", href);
$(img).closest = (sel) => {
$($img).closest = (sel) => {
if (sel === ".youtube-video") {
// We just need a nonempty array to
// set is_youtube_video to true.
@@ -82,13 +82,13 @@ test("youtube", ({override_rewire}) => {
return [];
};
img.set_parent(link);
link.closest = () => msg;
link.attr("href", href);
$img.set_parent($link);
$link.closest = () => $msg;
$link.attr("href", href);
override_rewire(lightbox, "render_lightbox_list_images", () => {});
const open_image = lightbox.build_open_image_function();
open_image(img);
open_image($img);
assert.equal($(".image-actions .open").attr("href"), href);
});

View File

@@ -70,12 +70,12 @@ run_test("single item list", ({override}) => {
});
const cursor = new ListCursor(conf);
const li_stub = {
const $li_stub = {
length: 1,
addClass: () => {},
};
override(conf.list, "find_li", () => li_stub);
override(conf.list, "find_li", () => $li_stub);
override(cursor, "adjust_scroll", () => {});
cursor.go_to(valid_key);

View File

@@ -41,81 +41,81 @@ const ListWidget = zrequire("list_widget");
// in the real code.
function make_container() {
const container = {};
const $container = {};
container.length = () => 1;
container.is = () => false;
container.css = (prop) => {
$container.length = () => 1;
$container.is = () => false;
$container.css = (prop) => {
assert.equal(prop, "max-height");
return "none";
};
// Make our append function just set a field we can
// check in our tests.
container.append = (data) => {
container.appended_data = data;
$container.append = ($data) => {
$container.$appended_data = $data;
};
return container;
return $container;
}
function make_scroll_container() {
const scroll_container = {};
const $scroll_container = {};
scroll_container.cleared = false;
$scroll_container.cleared = false;
// Capture the scroll callback so we can call it in
// our tests.
scroll_container.on = (ev, f) => {
$scroll_container.on = (ev, f) => {
assert.equal(ev, "scroll.list_widget_container");
scroll_container.call_scroll = () => {
f.call(scroll_container);
$scroll_container.call_scroll = () => {
f.call($scroll_container);
};
};
scroll_container.off = (ev) => {
$scroll_container.off = (ev) => {
assert.equal(ev, "scroll.list_widget_container");
scroll_container.cleared = true;
$scroll_container.cleared = true;
};
return scroll_container;
return $scroll_container;
}
function make_sort_container() {
const sort_container = {};
const $sort_container = {};
sort_container.cleared = false;
$sort_container.cleared = false;
sort_container.on = (ev, sel, f) => {
$sort_container.on = (ev, sel, f) => {
assert.equal(ev, "click.list_widget_sort");
assert.equal(sel, "[data-sort]");
sort_container.f = f;
$sort_container.f = f;
};
sort_container.off = (ev) => {
$sort_container.off = (ev) => {
assert.equal(ev, "click.list_widget_sort");
sort_container.cleared = true;
$sort_container.cleared = true;
};
return sort_container;
return $sort_container;
}
function make_filter_element() {
const element = {};
const $element = {};
element.cleared = false;
$element.cleared = false;
element.on = (ev, f) => {
$element.on = (ev, f) => {
assert.equal(ev, "input.list_widget_filter");
element.f = f;
$element.f = f;
};
element.off = (ev) => {
$element.off = (ev) => {
assert.equal(ev, "input.list_widget_filter");
element.cleared = true;
$element.cleared = true;
};
return element;
return $element;
}
function make_search_input() {
@@ -143,15 +143,15 @@ function div(item) {
}
run_test("scrolling", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
const items = [];
let get_scroll_element_called = false;
ui.get_scroll_element = (element) => {
ui.get_scroll_element = ($element) => {
get_scroll_element_called = true;
return element;
return $element;
};
for (let i = 0; i < 200; i += 1) {
@@ -160,38 +160,38 @@ run_test("scrolling", () => {
const opts = {
modifier: (item) => item,
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
};
container.html = (html) => {
$container.html = (html) => {
assert.equal(html, "");
};
ListWidget.create(container, items, opts);
ListWidget.create($container, items, opts);
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
assert.equal(get_scroll_element_called, true);
// Set up our fake geometry so it forces a scroll action.
scroll_container.scrollTop = 180;
scroll_container.clientHeight = 100;
scroll_container.scrollHeight = 260;
$scroll_container.scrollTop = 180;
$scroll_container.clientHeight = 100;
$scroll_container.scrollHeight = 260;
// Scrolling gets the next two elements from the list into
// our widget.
scroll_container.call_scroll();
assert.deepEqual(container.appended_data.html(), items.slice(80, 100).join(""));
$scroll_container.call_scroll();
assert.deepEqual($container.$appended_data.html(), items.slice(80, 100).join(""));
});
run_test("not_scrolling", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
const items = [];
let get_scroll_element_called = false;
ui.get_scroll_element = (element) => {
ui.get_scroll_element = ($element) => {
get_scroll_element_called = true;
return element;
return $element;
};
let post_scroll__pre_render_callback_called = false;
@@ -211,54 +211,54 @@ run_test("not_scrolling", () => {
const opts = {
modifier: (item) => item,
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
is_scroll_position_for_render: () => false,
post_scroll__pre_render_callback,
get_min_load_count,
};
container.html = (html) => {
$container.html = (html) => {
assert.equal(html, "");
};
ListWidget.create(container, items, opts);
ListWidget.create($container, items, opts);
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
assert.equal(get_scroll_element_called, true);
// Set up our fake geometry.
scroll_container.scrollTop = 180;
scroll_container.clientHeight = 100;
scroll_container.scrollHeight = 260;
$scroll_container.scrollTop = 180;
$scroll_container.clientHeight = 100;
$scroll_container.scrollHeight = 260;
// Since `should_render` is always false, no elements will be
// added regardless of scrolling.
scroll_container.call_scroll();
// appended_data remains the same.
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
$scroll_container.call_scroll();
// $appended_data remains the same.
assert.deepEqual($container.$appended_data.html(), items.slice(0, 80).join(""));
assert.equal(post_scroll__pre_render_callback_called, true);
assert.equal(get_min_load_count_called, true);
});
run_test("filtering", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
const search_input = make_search_input();
const $search_input = make_search_input();
const list = ["apple", "banana", "carrot", "dog", "egg", "fence", "grape"];
const opts = {
filter: {
element: search_input,
$element: $search_input,
predicate: (item, value) => item.includes(value),
},
modifier: (item) => div(item),
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
};
container.html = (html) => {
$container.html = (html) => {
assert.equal(html, "");
};
const widget = ListWidget.create(container, list, opts);
const widget = ListWidget.create($container, list, opts);
let expected_html =
"<div>apple</div>" +
@@ -269,45 +269,45 @@ run_test("filtering", () => {
"<div>fence</div>" +
"<div>grape</div>";
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
// Filtering will pick out dog/egg/grape when we put "g"
// into our search input. (This uses the default filter, which
// is a glorified indexOf call.)
search_input.val = () => "g";
search_input.simulate_input_event();
$search_input.val = () => "g";
$search_input.simulate_input_event();
assert.deepEqual(widget.get_current_list(), ["dog", "egg", "grape"]);
expected_html = "<div>dog</div><div>egg</div><div>grape</div>";
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
// We can insert new data into the widget.
const new_data = ["greta", "faye", "gary", "frank", "giraffe", "fox"];
widget.replace_list_data(new_data);
expected_html = "<div>greta</div><div>gary</div><div>giraffe</div>";
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
});
run_test("no filtering", () => {
const container = make_container();
const scroll_container = make_scroll_container();
container.html = () => {};
const $container = make_container();
const $scroll_container = make_scroll_container();
$container.html = () => {};
let callback_called = false;
// Opts does not require a filter key.
const opts = {
modifier: (item) => div(item),
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
callback_after_render: () => {
callback_called = true;
},
};
const widget = ListWidget.create(container, ["apple", "banana"], opts);
const widget = ListWidget.create($container, ["apple", "banana"], opts);
widget.render();
assert.deepEqual(callback_called, true);
const expected_html = "<div>apple</div><div>banana</div>";
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
});
function sort_button(opts) {
@@ -336,7 +336,7 @@ function sort_button(opts) {
const classList = new Set();
const button = {
const $button = {
data,
closest: lookup(".progressive-table-wrapper", {
data: lookup("list-widget", opts.list_name),
@@ -351,47 +351,47 @@ function sort_button(opts) {
siblings: lookup(".active", {
removeClass: (cls) => {
assert.equal(cls, "active");
button.siblings_deactivated = true;
$button.siblings_deactivated = true;
},
}),
siblings_deactivated: false,
to_jquery: () => button,
to_jquery: () => $button,
};
return button;
return $button;
}
run_test("wire up filter element", () => {
const lst = ["alice", "JESSE", "moses", "scott", "Sean", "Xavier"];
const container = make_container();
const scroll_container = make_scroll_container();
const filter_element = make_filter_element();
const $container = make_container();
const $scroll_container = make_scroll_container();
const $filter_element = make_filter_element();
// We don't care about what gets drawn initially.
container.html = () => {};
$container.html = () => {};
const opts = {
filter: {
filterer: (list, value) => list.filter((item) => item.toLowerCase().includes(value)),
element: filter_element,
$element: $filter_element,
},
modifier: (s) => "(" + s + ")",
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
};
ListWidget.create(container, lst, opts);
filter_element.f.apply({value: "se"});
assert.equal(container.appended_data.html(), "(JESSE)(moses)(Sean)");
ListWidget.create($container, lst, opts);
$filter_element.f.apply({value: "se"});
assert.equal($container.$appended_data.html(), "(JESSE)(moses)(Sean)");
});
run_test("sorting", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const sort_container = make_sort_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
const $sort_container = make_sort_container();
let cleared;
container.html = (html) => {
$container.html = (html) => {
assert.equal(html, "");
cleared = true;
};
@@ -406,22 +406,22 @@ run_test("sorting", () => {
const opts = {
name: "sorting-list",
parent_container: sort_container,
$parent_container: $sort_container,
modifier: (item) => div(item.name) + div(item.salary),
filter: {
predicate: () => true,
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
};
function html_for(people) {
return people.map((item) => opts.modifier(item)).join("");
}
ListWidget.create(container, list, opts);
ListWidget.create($container, list, opts);
let button_opts;
let button;
let $button;
let expected_html;
button_opts = {
@@ -431,31 +431,31 @@ run_test("sorting", () => {
active: false,
};
button = sort_button(button_opts);
$button = sort_button(button_opts);
sort_container.f.apply(button);
$sort_container.f.apply($button);
assert.ok(cleared);
assert.ok(button.siblings_deactivated);
assert.ok($button.siblings_deactivated);
expected_html = html_for([alice, bob, cal, dave, ellen]);
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
// Hit same button again to reverse the data.
cleared = false;
sort_container.f.apply(button);
$sort_container.f.apply($button);
assert.ok(cleared);
expected_html = html_for([ellen, dave, cal, bob, alice]);
assert.deepEqual(container.appended_data.html(), expected_html);
assert.ok(button.hasClass("descend"));
assert.deepEqual($container.$appended_data.html(), expected_html);
assert.ok($button.hasClass("descend"));
// And then hit a third time to go back to the forward sort.
cleared = false;
sort_container.f.apply(button);
$sort_container.f.apply($button);
assert.ok(cleared);
expected_html = html_for([alice, bob, cal, dave, ellen]);
assert.deepEqual(container.appended_data.html(), expected_html);
assert.ok(!button.hasClass("descend"));
assert.deepEqual($container.$appended_data.html(), expected_html);
assert.ok(!$button.hasClass("descend"));
// Now try a numeric sort.
button_opts = {
@@ -465,32 +465,32 @@ run_test("sorting", () => {
active: false,
};
button = sort_button(button_opts);
$button = sort_button(button_opts);
cleared = false;
button.siblings_deactivated = false;
$button.siblings_deactivated = false;
sort_container.f.apply(button);
$sort_container.f.apply($button);
assert.ok(cleared);
assert.ok(button.siblings_deactivated);
assert.ok($button.siblings_deactivated);
expected_html = html_for([dave, cal, bob, alice, ellen]);
assert.deepEqual(container.appended_data.html(), expected_html);
assert.deepEqual($container.$appended_data.html(), expected_html);
// Hit same button again to reverse the numeric sort.
cleared = false;
sort_container.f.apply(button);
$sort_container.f.apply($button);
assert.ok(cleared);
expected_html = html_for([ellen, alice, bob, cal, dave]);
assert.deepEqual(container.appended_data.html(), expected_html);
assert.ok(button.hasClass("descend"));
assert.deepEqual($container.$appended_data.html(), expected_html);
assert.ok($button.hasClass("descend"));
});
run_test("custom sort", () => {
const container = make_container();
const scroll_container = make_scroll_container();
container.html = () => {};
const $container = make_container();
const $scroll_container = make_scroll_container();
$container.html = () => {};
const n42 = {x: 6, y: 7};
const n43 = {x: 1, y: 43};
@@ -506,7 +506,7 @@ run_test("custom sort", () => {
return a.x * a.y - b.x * b.y;
}
ListWidget.create(container, list, {
ListWidget.create($container, list, {
name: "custom-sort-list",
modifier: (n) => "(" + n.x + ", " + n.y + ")",
sort_fields: {
@@ -514,15 +514,15 @@ run_test("custom sort", () => {
x_value: sort_by_x,
},
init_sort: [sort_by_product],
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
assert.deepEqual(container.appended_data.html(), "(6, 7)(1, 43)(4, 11)");
assert.deepEqual($container.$appended_data.html(), "(6, 7)(1, 43)(4, 11)");
const widget = ListWidget.get("custom-sort-list");
widget.sort("x_value");
assert.deepEqual(container.appended_data.html(), "(1, 43)(4, 11)(6, 7)");
assert.deepEqual($container.$appended_data.html(), "(1, 43)(4, 11)(6, 7)");
// We can sort without registering the function, too.
function sort_by_y(a, b) {
@@ -530,97 +530,97 @@ run_test("custom sort", () => {
}
widget.sort(sort_by_y);
assert.deepEqual(container.appended_data.html(), "(6, 7)(4, 11)(1, 43)");
assert.deepEqual($container.$appended_data.html(), "(6, 7)(4, 11)(1, 43)");
});
run_test("clear_event_handlers", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const sort_container = make_sort_container();
const filter_element = make_filter_element();
const $container = make_container();
const $scroll_container = make_scroll_container();
const $sort_container = make_sort_container();
const $filter_element = make_filter_element();
// We don't care about actual data for this test.
const list = [];
container.html = () => {};
$container.html = () => {};
const opts = {
name: "list-we-create-twice",
parent_container: sort_container,
$parent_container: $sort_container,
modifier: () => {},
filter: {
element: filter_element,
$element: $filter_element,
predicate: () => true,
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
};
// Create it the first time.
ListWidget.create(container, list, opts);
assert.equal(sort_container.cleared, false);
assert.equal(scroll_container.cleared, false);
assert.equal(filter_element.cleared, false);
ListWidget.create($container, list, opts);
assert.equal($sort_container.cleared, false);
assert.equal($scroll_container.cleared, false);
assert.equal($filter_element.cleared, false);
// The second time we'll clear the old events.
ListWidget.create(container, list, opts);
assert.equal(sort_container.cleared, true);
assert.equal(scroll_container.cleared, true);
assert.equal(filter_element.cleared, true);
ListWidget.create($container, list, opts);
assert.equal($sort_container.cleared, true);
assert.equal($scroll_container.cleared, true);
assert.equal($filter_element.cleared, true);
});
run_test("errors", () => {
// We don't care about actual data for this test.
const list = ["stub"];
const container = make_container();
const scroll_container = make_scroll_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
blueslip.expect("error", "Need opts to create widget.");
ListWidget.create(container, list);
ListWidget.create($container, list);
blueslip.reset();
blueslip.expect("error", "simplebar_container is missing.");
ListWidget.create(container, list, {
blueslip.expect("error", "$simplebar_container is missing.");
ListWidget.create($container, list, {
modifier: "hello world",
});
blueslip.reset();
blueslip.expect("error", "get_item should be a function");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
get_item: "not a function",
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
blueslip.expect("error", "Filter predicate is not a function.");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
filter: {
predicate: "wrong type",
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
blueslip.expect("error", "Filterer and predicate are mutually exclusive.");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
filter: {
filterer: () => true,
predicate: () => true,
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
blueslip.expect("error", "Filter filterer is not a function (or missing).");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
filter: {},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
container.html = () => {};
$container.html = () => {};
blueslip.expect("error", "List item is not a string: 999");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
modifier: () => 999,
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
});
@@ -648,14 +648,14 @@ run_test("sort helpers", () => {
});
run_test("replace_list_data w/filter update", () => {
const container = make_container();
const scroll_container = make_scroll_container();
container.html = () => {};
const $container = make_container();
const $scroll_container = make_scroll_container();
$container.html = () => {};
const list = [1, 2, 3, 4];
let num_updates = 0;
ListWidget.create(container, list, {
ListWidget.create($container, list, {
name: "replace-list",
modifier: (n) => "(" + n.toString() + ")",
filter: {
@@ -664,19 +664,19 @@ run_test("replace_list_data w/filter update", () => {
num_updates += 1;
},
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
assert.equal(num_updates, 0);
assert.deepEqual(container.appended_data.html(), "(2)(4)");
assert.deepEqual($container.$appended_data.html(), "(2)(4)");
const widget = ListWidget.get("replace-list");
widget.replace_list_data([5, 6, 7, 8]);
assert.equal(num_updates, 1);
assert.deepEqual(container.appended_data.html(), "(6)(8)");
assert.deepEqual($container.$appended_data.html(), "(6)(8)");
});
run_test("opts.get_item", () => {
@@ -722,12 +722,12 @@ run_test("opts.get_item", () => {
});
run_test("render item", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const $container = make_container();
const $scroll_container = make_scroll_container();
const INITIAL_RENDER_COUNT = 80; // Keep this in sync with the actual code.
container.html = () => {};
$container.html = () => {};
let called = false;
scroll_container.find = (query) => {
$scroll_container.find = (query) => {
const expected_queries = [
`tr[data-item='${INITIAL_RENDER_COUNT}']`,
`tr[data-item='${INITIAL_RENDER_COUNT - 1}']`,
@@ -746,7 +746,7 @@ run_test("render item", () => {
replaceWith: (html) => {
assert.equal(new_html, html);
called = true;
container.appended_data.replace(regex, new_html);
$container.$appended_data.replace(regex, new_html);
},
};
};
@@ -756,29 +756,29 @@ run_test("render item", () => {
let text = "initial";
const get_item = (item) => ({text: `${text}: ${item}`, value: item});
const widget = ListWidget.create(container, list, {
const widget = ListWidget.create($container, list, {
name: "replace-list",
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item,
html_selector: (item) => `tr[data-item='${item}']`,
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
const item = INITIAL_RENDER_COUNT - 1;
assert.ok(container.appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
assert.ok(container.appended_data.html().includes("<tr data-item=3>initial: 3</tr>"));
assert.ok($container.$appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
assert.ok($container.$appended_data.html().includes("<tr data-item=3>initial: 3</tr>"));
text = "updated";
called = false;
widget.render_item(INITIAL_RENDER_COUNT - 1);
assert.ok(called);
assert.ok(container.appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
assert.ok($container.$appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
assert.ok(
container.appended_data.html().includes(`<tr data-item=${item}>updated: ${item}</tr>`),
$container.$appended_data.html().includes(`<tr data-item=${item}>updated: ${item}</tr>`),
);
// Item 80 should not be in the rendered list. (0 indexed)
assert.ok(
!container.appended_data
!$container.$appended_data
.html()
.includes(
`<tr data-item=${INITIAL_RENDER_COUNT}>initial: ${INITIAL_RENDER_COUNT}</tr>`,
@@ -793,24 +793,24 @@ run_test("render item", () => {
// Tests below this are for the corner cases, where we abort the rerender.
blueslip.expect("error", "html_selector should be a function.");
ListWidget.create(container, list, {
ListWidget.create($container, list, {
name: "replace-list",
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item,
html_selector: "hello world",
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
blueslip.reset();
let get_item_called;
const widget_2 = ListWidget.create(container, list, {
const widget_2 = ListWidget.create($container, list, {
name: "replace-list",
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
get_item: (item) => {
get_item_called = true;
return item;
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
get_item_called = false;
widget_2.render_item(item);
@@ -818,12 +818,12 @@ run_test("render item", () => {
assert.ok(!get_item_called);
let rendering_item = false;
const widget_3 = ListWidget.create(container, list, {
const widget_3 = ListWidget.create($container, list, {
name: "replace-list",
modifier: (item) => (rendering_item ? undefined : `${item}\n`),
get_item,
html_selector: (item) => `tr[data-item='${item}']`,
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
// Once we have initially rendered the widget, change the
// behavior of the modifier function.
@@ -834,16 +834,16 @@ run_test("render item", () => {
});
run_test("Multiselect dropdown retain_selected_items", () => {
const container = make_container();
const scroll_container = make_scroll_container();
const filter_element = make_filter_element();
const $container = make_container();
const $scroll_container = make_scroll_container();
const $filter_element = make_filter_element();
let data_rendered = [];
const list = ["one", "two", "three", "four"].map((x) => ({name: x, value: x}));
const data = ["one"]; // Data initially selected.
container.html = () => {};
container.find = (elem) => DropdownItem(elem);
$container.html = () => {};
$container.find = (elem) => DropdownItem(elem);
// We essentially create fake jQuery functions
// whose return value are stored in objects so that
@@ -880,8 +880,8 @@ run_test("Multiselect dropdown retain_selected_items", () => {
return ListItem(element, temp);
}
function prepend(data) {
temp.prepended_data = data.html();
function prepend($data) {
temp.prepended_data = $data.html();
}
return {
@@ -890,17 +890,17 @@ run_test("Multiselect dropdown retain_selected_items", () => {
};
}
const widget = ListWidget.create(container, list, {
const widget = ListWidget.create($container, list, {
name: "replace-list",
modifier: (item) => `<li data-value="${item.value}">${item.name}</li>\n`,
multiselect: {
selected_items: data,
},
filter: {
element: filter_element,
$element: $filter_element,
predicate: () => true,
},
simplebar_container: scroll_container,
$simplebar_container: $scroll_container,
});
const expected_value = [

View File

@@ -268,18 +268,15 @@ test("marked_shared", () => {
test("message_flags", () => {
let message = {raw_content: "@**Leo**"};
markdown.apply_markdown(message);
assert.ok(!message.mentioned);
assert.ok(!message.mentioned_me_directly);
assert.ok(!message.flags.includes("mentioned"));
message = {raw_content: "@**Cordelia, Lear's daughter**"};
markdown.apply_markdown(message);
assert.ok(message.mentioned);
assert.ok(message.mentioned_me_directly);
assert.ok(message.flags.includes("mentioned"));
message = {raw_content: "@**all**"};
markdown.apply_markdown(message);
assert.ok(message.mentioned);
assert.ok(!message.mentioned_me_directly);
assert.ok(message.flags.includes("wildcard_mentioned"));
});
test("marked", () => {
@@ -680,77 +677,87 @@ test("message_flags", () => {
markdown.apply_markdown(message);
assert.equal(message.is_me_message, false);
assert.equal(message.mentioned, true);
assert.equal(message.mentioned_me_directly, true);
assert.equal(message.flags.includes("mentioned"), true);
assert.equal(message.flags.includes("wildcard_mentioned"), true);
input = "test @**everyone**";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.is_me_message, false);
assert.equal(message.mentioned, true);
assert.equal(message.mentioned_me_directly, false);
assert.equal(message.flags.includes("wildcard_mentioned"), true);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @**stream**";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.is_me_message, false);
assert.equal(message.mentioned, true);
assert.equal(message.mentioned_me_directly, false);
assert.equal(message.flags.includes("wildcard_mentioned"), true);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @all";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @everyone";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @any";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @alleycat.com";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @*hamletcharacters*";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, true);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), true);
input = "test @*backend*";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @**invalid_user**";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @_**all**";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "> test @**all**";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "test @_*hamletcharacters*";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
input = "> test @*hamletcharacters*";
message = {topic: "No links here", raw_content: input};
markdown.apply_markdown(message);
assert.equal(message.mentioned, false);
assert.equal(message.flags.includes("wildcard_mentioned"), false);
assert.equal(message.flags.includes("mentioned"), false);
});
test("backend_only_linkifiers", () => {

View File

@@ -0,0 +1,149 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const markdown = zrequire("markdown");
const my_id = 101;
const user_map = new Map();
user_map.set(my_id, "Me Myself");
user_map.set(105, "greg");
function get_actual_name_from_user_id(user_id) {
return user_map.get(user_id);
}
function get_user_id_from_name(name) {
for (const [user_id, _name] of user_map.entries()) {
if (name === _name) {
return user_id;
}
}
return undefined;
}
function is_valid_full_name_and_user_id(name, user_id) {
return user_map.has(user_id) && user_map.get(user_id) === name;
}
function my_user_id() {
return my_id;
}
function is_valid_user_id(user_id) {
return user_map.has(user_id);
}
const staff_group = {
id: 201,
name: "Staff",
};
const user_group_map = new Map();
user_group_map.set(staff_group.name, staff_group);
function get_user_group_from_name(name) {
return user_group_map.get(name);
}
function is_member_of_user_group(user_group_id, user_id) {
assert.equal(user_group_id, staff_group.id);
assert.equal(user_id, my_id);
return true;
}
const social = {
stream_id: 301,
name: "social",
};
const sub_map = new Map();
sub_map.set(social.name, social);
function get_stream_by_name(name) {
return sub_map.get(name);
}
function stream_hash(stream_id) {
return `stream-${stream_id}`;
}
function stream_topic_hash(stream_id, topic) {
return `stream-${stream_id}-topic-${topic}`;
}
const helper_config = {
// user stuff
get_actual_name_from_user_id,
get_user_id_from_name,
is_valid_full_name_and_user_id,
is_valid_user_id,
my_user_id,
// user groups
get_user_group_from_name,
is_member_of_user_group,
// stream hashes
get_stream_by_name,
stream_hash,
stream_topic_hash,
// settings
should_translate_emoticons: () => false,
};
function assert_parse(raw_content, expected_content) {
const {content} = markdown.parse({raw_content, helper_config});
assert.equal(content, expected_content);
}
function test(label, f) {
markdown.setup();
run_test(label, f);
}
test("basics", () => {
assert_parse("boring", "<p>boring</p>");
assert_parse("**bold**", "<p><strong>bold</strong></p>");
});
test("user mentions", () => {
assert_parse("@**greg**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse("@**|105**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse(
"@**greg|105**",
'<p><span class="user-mention" data-user-id="105">@greg</span></p>',
);
assert_parse(
"@**Me Myself|101**",
'<p><span class="user-mention" data-user-id="101">@Me Myself</span></p>',
);
});
test("user group mentions", () => {
assert_parse(
"@*Staff*",
'<p><span class="user-group-mention" data-user-group-id="201">@Staff</span></p>',
);
});
test("stream links", () => {
assert_parse(
"#**social**",
'<p><a class="stream" data-stream-id="301" href="/stream-301">#social</a></p>',
);
assert_parse(
"#**social>lunch**",
'<p><a class="stream-topic" data-stream-id="301" href="/stream-301-topic-lunch">#social &gt; lunch</a></p>',
);
});

View File

@@ -158,3 +158,114 @@ run_test("get_deletability", ({override}) => {
message.timestamp = current_timestamp - 60;
assert.equal(message_edit.get_deletability(message), false);
});
run_test("stream_and_topic_exist_in_edit_history", () => {
// A message with no edit history should always return false;
// the message's current stream_id and topic are not compared
// to the stream_id and topic parameters.
const message_no_edits = {
stream_id: 1,
topic: "topic match",
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_no_edits, 2, "no match"),
false,
);
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_no_edits, 1, "topic match"),
false,
);
// A non-stream message (object has no stream_id or topic)
// with content edit history, should return false.
const private_message = {
edit_history: [{prev_content: "content edit to PM"}],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(private_message, 1, "topic match"),
false,
);
// A stream message with only content edits should return false,
// even if the message's current stream_id and topic are a match.
const message_content_edit = {
stream_id: 1,
topic: "topic match",
edit_history: [{prev_content: "content edit"}],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_content_edit, 1, "topic match"),
false,
);
const message_stream_edit = {
stream_id: 6,
topic: "topic match",
edit_history: [{stream: 6, prev_stream: 1}],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_stream_edit, 2, "topic match"),
false,
);
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_stream_edit, 1, "topic match"),
true,
);
const message_topic_edit = {
stream_id: 1,
topic: "final topic",
edit_history: [{topic: "final topic", prev_topic: "topic match"}],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_topic_edit, 1, "no match"),
false,
);
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_topic_edit, 1, "topic match"),
true,
);
const message_many_edits = {
stream_id: 6,
topic: "final topic",
edit_history: [
{stream: 6, prev_stream: 5},
{prev_content: "content only edit"},
{topic: "final topic", prev_topic: "topic match"},
{stream: 5, prev_stream: 1},
],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_many_edits, 1, "no match"),
false,
);
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_many_edits, 2, "topic match"),
false,
);
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(message_many_edits, 1, "topic match"),
true,
);
// When the topic and stream_id exist in the message's edit history
// individually, but not together in a historical state, it should return false.
const message_no_historical_match = {
stream_id: 6,
topic: "final topic",
edit_history: [
{stream: 6, prev_stream: 1}, // stream matches, topic does not
{stream: 1, prev_stream: 5}, // neither match
{topic: "final topic", prev_topic: "topic match"}, // topic matches, stream does not
],
};
assert.equal(
message_edit.stream_and_topic_exist_in_edit_history(
message_no_historical_match,
1,
"topic match",
),
false,
);
});

View File

@@ -20,7 +20,6 @@ message_lists.current = {};
const people = zrequire("people");
const message_events = zrequire("message_events");
const message_helper = zrequire("message_helper");
const message_store = zrequire("message_store");
const stream_data = zrequire("stream_data");
const stream_topic_history = zrequire("stream_topic_history");
const unread = zrequire("unread");
@@ -70,7 +69,6 @@ run_test("update_messages", () => {
};
message_helper.process_new_message(original_message);
message_store.set_message_booleans(original_message);
assert.equal(original_message.mentioned, true);
assert.equal(original_message.unread, true);
@@ -117,9 +115,9 @@ run_test("update_messages", () => {
page_params.realm_allow_edit_history = false;
message_list.narrowed = "stub-to-ignore";
const message_edit_history_modal = $.create("#message-edit-history");
const modal = $.create("micromodal").addClass("modal--open");
message_edit_history_modal.set_parents_result(".micromodal", modal);
const $message_edit_history_modal = $.create("#message-edit-history");
const $modal = $.create("micromodal").addClass("modal--open");
$message_edit_history_modal.set_parents_result(".micromodal", $modal);
// TEST THIS:
message_events.update_messages(events);

View File

@@ -31,7 +31,6 @@ mock_esm("../../static/js/ui_report", {
const channel = mock_esm("../../static/js/channel");
const message_helper = mock_esm("../../static/js/message_helper");
const message_lists = mock_esm("../../static/js/message_lists");
const message_store = mock_esm("../../static/js/message_store");
const message_util = mock_esm("../../static/js/message_util");
const pm_list = mock_esm("../../static/js/pm_list");
const stream_list = mock_esm("../../static/js/stream_list", {
@@ -119,14 +118,13 @@ function config_fake_channel(conf) {
function config_process_results(messages) {
const self = {};
const messages_processed_for_bools = [];
const messages_processed_for_new = [];
message_store.set_message_booleans = (message) => {
messages_processed_for_bools.push(message);
message_helper.process_new_message = (message) => {
messages_processed_for_new.push(message);
return message;
};
message_helper.process_new_message = (message) => message;
message_util.do_unread_count_updates = (arg) => {
assert.deepEqual(arg, messages);
};
@@ -141,7 +139,7 @@ function config_process_results(messages) {
pm_list.update_private_messages = noop;
self.verify = () => {
assert.deepEqual(messages_processed_for_bools, messages);
assert.deepEqual(messages_processed_for_new, messages);
};
return self;

View File

@@ -110,7 +110,6 @@ test("process_new_message", () => {
is_me_message: false,
id: 2067,
};
message_store.set_message_booleans(message);
message_helper.process_new_message(message);
assert.deepEqual(message_user_ids.user_ids().sort(), [me.user_id, bob.user_id, cindy.user_id]);
@@ -153,7 +152,6 @@ test("process_new_message", () => {
id: 2068,
};
message_store.set_message_booleans(message);
message_helper.process_new_message(message);
assert.deepEqual(message.stream, message.display_recipient);
assert.equal(message.reply_to, "denise@example.com");
@@ -307,7 +305,6 @@ test("update_property", () => {
id: 101,
};
for (const message of [message1, message2]) {
message_store.set_message_booleans(message);
message_helper.process_new_message(message);
}

View File

@@ -449,7 +449,6 @@ run_test("show_empty_narrow_message_with_search", ({mock_template}) => {
});
run_test("hide_empty_narrow_message", () => {
$(".empty_feed_notice_main").html("<div class='empty_feed_notice'>Nothing here</div>");
narrow_banner.hide_empty_narrow_message();
assert.equal($(".empty_feed_notice").text(), "never-been-set");
});

View File

@@ -27,47 +27,47 @@ run_test("basics w/progress bar", () => {
let password;
let warning;
const bar = (function () {
const self = {};
const $bar = (function () {
const $self = {};
self.width = (width) => {
self.w = width;
return self;
$self.width = (width) => {
$self.w = width;
return $self;
};
self.removeClass = (arg) => {
$self.removeClass = (arg) => {
assert.equal(arg, "bar-success bar-danger");
return self;
return $self;
};
self.addClass = (arg) => {
self.added_class = arg;
return self;
$self.addClass = (arg) => {
$self.added_class = arg;
return $self;
};
return self;
return $self;
})();
password = "z!X4@S_&";
accepted = password_quality(password, bar, password_field(10, 80000));
accepted = password_quality(password, $bar, password_field(10, 80000));
assert.ok(!accepted);
assert.equal(bar.w, "39.7%");
assert.equal(bar.added_class, "bar-danger");
assert.equal($bar.w, "39.7%");
assert.equal($bar.added_class, "bar-danger");
warning = password_warning(password, password_field(10));
assert.equal(warning, "translated: Password should be at least 10 characters long");
password = "foo";
accepted = password_quality(password, bar, password_field(2, 200));
accepted = password_quality(password, $bar, password_field(2, 200));
assert.ok(accepted);
assert.equal(bar.w, "10.390277164940581%");
assert.equal(bar.added_class, "bar-success");
assert.equal($bar.w, "10.390277164940581%");
assert.equal($bar.added_class, "bar-success");
warning = password_warning(password, password_field(2));
assert.equal(warning, "translated: Password is too weak");
password = "aaaaaaaa";
accepted = password_quality(password, bar, password_field(6, 1e100));
accepted = password_quality(password, $bar, password_field(6, 1e100));
assert.ok(!accepted);
assert.equal(bar.added_class, "bar-danger");
assert.equal($bar.added_class, "bar-danger");
warning = password_warning(password, password_field(6));
assert.equal(warning, 'Repeats like "aaa" are easy to guess');
assert.equal(warning, 'Repeated characters like "aaa" are easy to guess.');
});

View File

@@ -19,19 +19,19 @@ const user_groups = zrequire("user_groups");
// set global test variables.
let sort_recipients_called = false;
let sort_streams_called = false;
const fake_rendered_person = $.create("fake-rendered-person");
const fake_rendered_stream = $.create("fake-rendered-stream");
const fake_rendered_group = $.create("fake-rendered-group");
const $fake_rendered_person = $.create("fake-rendered-person");
const $fake_rendered_stream = $.create("fake-rendered-stream");
const $fake_rendered_group = $.create("fake-rendered-group");
mock_esm("../../static/js/typeahead_helper", {
render_person() {
return fake_rendered_person;
return $fake_rendered_person;
},
render_user_group() {
return fake_rendered_group;
return $fake_rendered_group;
},
render_stream() {
return fake_rendered_stream;
return $fake_rendered_stream;
},
sort_streams() {
sort_streams_called = true;
@@ -113,20 +113,20 @@ run_test("set_up", ({mock_template}) => {
return html;
});
let input_pill_typeahead_called = false;
const fake_input = $.create(".input");
fake_input.before = noop;
const $fake_input = $.create(".input");
$fake_input.before = noop;
const container = $.create(".pill-container");
container.find = () => fake_input;
const $container = $.create(".pill-container");
$container.find = () => $fake_input;
const pill_widget = input_pill.create({
container,
const $pill_widget = input_pill.create({
$container,
create_item_from_text: noop,
get_text_from_item: noop,
});
let opts = {};
fake_input.typeahead = (config) => {
$fake_input.typeahead = (config) => {
assert.equal(config.items, 5);
assert.ok(config.fixed);
assert.ok(config.dropup);
@@ -154,7 +154,7 @@ run_test("set_up", ({mock_template}) => {
// Test stream highlighter for widgets that allow stream pills.
assert.equal(
config.highlighter.call(fake_stream_this, denmark),
fake_rendered_stream,
$fake_rendered_stream,
);
}
if (opts.user_group && opts.user) {
@@ -162,17 +162,17 @@ run_test("set_up", ({mock_template}) => {
// then we should check that each of them rendered correctly.
assert.equal(
config.highlighter.call(fake_group_this, testers),
fake_rendered_group,
$fake_rendered_group,
);
assert.equal(config.highlighter.call(fake_person_this, me), fake_rendered_person);
assert.equal(config.highlighter.call(fake_person_this, me), $fake_rendered_person);
}
if (opts.user && !opts.user_group) {
assert.equal(config.highlighter.call(fake_person_this, me), fake_rendered_person);
assert.equal(config.highlighter.call(fake_person_this, me), $fake_rendered_person);
}
if (!opts.user && opts.user_group) {
assert.equal(
config.highlighter.call(fake_group_this, testers),
fake_rendered_group,
$fake_rendered_group,
);
}
})();
@@ -288,7 +288,7 @@ run_test("set_up", ({mock_template}) => {
// updater in pill_typeahead.
function number_of_pills() {
const pills = pill_widget.items();
const pills = $pill_widget.items();
return pills.length;
}
assert.equal(number_of_pills(), 0);
@@ -307,7 +307,7 @@ run_test("set_up", ({mock_template}) => {
};
function test_pill_typeahead(opts) {
pill_typeahead.set_up(fake_input, pill_widget, opts);
pill_typeahead.set_up($fake_input, $pill_widget, opts);
assert.ok(input_pill_typeahead_called);
}
@@ -339,6 +339,6 @@ run_test("set_up", ({mock_template}) => {
opts = {};
input_pill_typeahead_called = false;
blueslip.expect("error", "Unspecified possible item types");
pill_typeahead.set_up(fake_input, pill_widget, {});
pill_typeahead.set_up($fake_input, $pill_widget, {});
assert.ok(!input_pill_typeahead_called);
});

View File

@@ -2,36 +2,37 @@
const {strict: assert} = require("assert");
const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const $ = require("../zjsunit/zjquery");
const narrow_state = mock_esm("../../static/js/narrow_state");
const narrow_state = zrequire("narrow_state");
const pm_list = zrequire("pm_list");
run_test("update_dom_with_unread_counts", ({override}) => {
run_test("update_dom_with_unread_counts", () => {
let counts;
override(narrow_state, "active", () => true);
// simulate an active narrow
narrow_state.set_current_filter("stub");
assert.equal(narrow_state.active(), true);
const total_count = $.create("total-count-stub");
const private_li = $(".top_left_private_messages .private_messages_header");
private_li.set_find_results(".unread_count", total_count);
const $total_count = $.create("total-count-stub");
const $private_li = $(".top_left_private_messages .private_messages_header");
$private_li.set_find_results(".unread_count", $total_count);
counts = {
private_message_count: 10,
};
pm_list.update_dom_with_unread_counts(counts);
assert.equal(total_count.text(), "10");
assert.ok(total_count.visible());
assert.equal($total_count.text(), "10");
assert.ok($total_count.visible());
counts = {
private_message_count: 0,
};
pm_list.update_dom_with_unread_counts(counts);
assert.equal(total_count.text(), "");
assert.ok(!total_count.visible());
assert.equal($total_count.text(), "");
assert.ok(!$total_count.visible());
});

View File

@@ -5,7 +5,6 @@ const {strict: assert} = require("assert");
const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const narrow_state = mock_esm("../../static/js/narrow_state");
const unread = mock_esm("../../static/js/unread");
mock_esm("../../static/js/user_status", {
@@ -15,6 +14,7 @@ mock_esm("../../static/js/user_status", {
}),
});
const narrow_state = zrequire("narrow_state");
const people = zrequire("people");
const pm_conversations = zrequire("pm_conversations");
const pm_list_data = zrequire("pm_list_data");
@@ -49,6 +49,7 @@ people.initialize_current_user(me.user_id);
function test(label, f) {
run_test(label, ({override, override_rewire}) => {
narrow_state.reset_current_filter();
pm_conversations.clear_for_testing();
f({override, override_rewire});
});
@@ -60,7 +61,7 @@ test("get_convos", ({override}) => {
let num_unread_for_person = 1;
override(unread, "num_unread_for_person", () => num_unread_for_person);
override(narrow_state, "filter", () => {});
assert.equal(narrow_state.filter(), undefined);
const expected_data = [
{
@@ -111,7 +112,7 @@ test("get_convos bot", ({override}) => {
override(unread, "num_unread_for_person", () => 1);
override(narrow_state, "filter", () => {});
assert.equal(narrow_state.filter(), undefined);
const expected_data = [
{
@@ -142,20 +143,17 @@ test("get_convos bot", ({override}) => {
assert.deepEqual(pm_data, expected_data);
});
test("get_active_user_ids_string", ({override}) => {
let active_filter;
override(narrow_state, "filter", () => active_filter);
test("get_active_user_ids_string", () => {
assert.equal(pm_list_data.get_active_user_ids_string(), undefined);
function set_filter_result(emails) {
active_filter = {
const active_filter = {
operands: (operand) => {
assert.equal(operand, "pm-with");
return emails;
},
};
narrow_state.set_current_filter(active_filter);
}
set_filter_result([]);
@@ -174,13 +172,10 @@ function private_filter() {
};
}
test("is_all_privates", ({override}) => {
let filter;
override(narrow_state, "filter", () => filter);
filter = undefined;
test("is_all_privates", () => {
assert.equal(narrow_state.filter(), undefined);
assert.equal(pm_list_data.is_all_privates(), false);
filter = private_filter();
narrow_state.set_current_filter(private_filter());
assert.equal(pm_list_data.is_all_privates(), true);
});

View File

@@ -210,7 +210,7 @@ run_test("activate another person poll", ({mock_template}) => {
mock_template("widgets/poll_widget.hbs", false, () => "widgets/poll_widget");
mock_template("widgets/poll_widget_results.hbs", false, () => "widgets/poll_widget_results");
const widget_elem = $("<div>").addClass("widget-content");
const $widget_elem = $("<div>").addClass("widget-content");
let out_data; // Used to check the event data sent to the server
const callback = (data) => {
@@ -218,7 +218,7 @@ run_test("activate another person poll", ({mock_template}) => {
};
const opts = {
elem: widget_elem,
$elem: $widget_elem,
callback,
message: {
sender_id: alice.user_id,
@@ -229,53 +229,53 @@ run_test("activate another person poll", ({mock_template}) => {
};
const set_widget_find_result = (selector) => {
const elem = $.create(selector);
widget_elem.set_find_results(selector, elem);
return elem;
const $elem = $.create(selector);
$widget_elem.set_find_results(selector, $elem);
return $elem;
};
const poll_option = set_widget_find_result("button.poll-option");
const poll_option_input = set_widget_find_result("input.poll-option");
const widget_option_container = set_widget_find_result("ul.poll-widget");
const $poll_option = set_widget_find_result("button.poll-option");
const $poll_option_input = set_widget_find_result("input.poll-option");
const $widget_option_container = set_widget_find_result("ul.poll-widget");
const poll_question_submit = set_widget_find_result("button.poll-question-check");
const poll_edit_question = set_widget_find_result(".poll-edit-question");
const poll_question_header = set_widget_find_result(".poll-question-header");
const poll_question_container = set_widget_find_result(".poll-question-bar");
const poll_option_container = set_widget_find_result(".poll-option-bar");
const $poll_question_submit = set_widget_find_result("button.poll-question-check");
const $poll_edit_question = set_widget_find_result(".poll-edit-question");
const $poll_question_header = set_widget_find_result(".poll-question-header");
const $poll_question_container = set_widget_find_result(".poll-question-bar");
const $poll_option_container = set_widget_find_result(".poll-option-bar");
const poll_vote_button = set_widget_find_result("button.poll-vote");
const poll_please_wait = set_widget_find_result(".poll-please-wait");
const poll_author_help = set_widget_find_result(".poll-author-help");
const $poll_vote_button = set_widget_find_result("button.poll-vote");
const $poll_please_wait = set_widget_find_result(".poll-please-wait");
const $poll_author_help = set_widget_find_result(".poll-author-help");
set_widget_find_result("button.poll-question-remove");
set_widget_find_result("input.poll-question");
poll_widget.activate(opts);
assert.ok(poll_option_container.visible());
assert.ok(poll_question_header.visible());
assert.ok($poll_option_container.visible());
assert.ok($poll_question_header.visible());
assert.ok(!poll_question_container.visible());
assert.ok(!poll_question_submit.visible());
assert.ok(!poll_edit_question.visible());
assert.ok(!poll_please_wait.visible());
assert.ok(!poll_author_help.visible());
assert.ok(!$poll_question_container.visible());
assert.ok(!$poll_question_submit.visible());
assert.ok(!$poll_edit_question.visible());
assert.ok(!$poll_please_wait.visible());
assert.ok(!$poll_author_help.visible());
assert.equal(widget_elem.html(), "widgets/poll_widget");
assert.equal(widget_option_container.html(), "widgets/poll_widget_results");
assert.equal(poll_question_header.text(), "What do you want?");
assert.equal($widget_elem.html(), "widgets/poll_widget");
assert.equal($widget_option_container.html(), "widgets/poll_widget_results");
assert.equal($poll_question_header.text(), "What do you want?");
{
/* Testing data sent to server on adding option */
poll_option_input.val("cool choice");
$poll_option_input.val("cool choice");
out_data = undefined;
poll_option.trigger("click");
$poll_option.trigger("click");
assert.deepEqual(out_data, {type: "new_option", idx: 1, option: "cool choice"});
poll_option_input.val("");
$poll_option_input.val("");
out_data = undefined;
poll_option.trigger("click");
$poll_option.trigger("click");
assert.deepEqual(out_data, undefined);
}
@@ -298,13 +298,13 @@ run_test("activate another person poll", ({mock_template}) => {
},
];
widget_elem.handle_events(vote_events);
$widget_elem.handle_events(vote_events);
{
/* Testing data sent to server on voting */
poll_vote_button.attr("data-key", "100,1");
$poll_vote_button.attr("data-key", "100,1");
out_data = undefined;
poll_vote_button.trigger("click");
$poll_vote_button.trigger("click");
assert.deepEqual(out_data, {type: "vote", key: "100,1", vote: 1});
}
@@ -318,20 +318,20 @@ run_test("activate another person poll", ({mock_template}) => {
},
];
widget_elem.handle_events(add_question_event);
$widget_elem.handle_events(add_question_event);
});
run_test("activate own poll", ({mock_template}) => {
mock_template("widgets/poll_widget.hbs", false, () => "widgets/poll_widget");
mock_template("widgets/poll_widget_results.hbs", false, () => "widgets/poll_widget_results");
const widget_elem = $("<div>").addClass("widget-content");
const $widget_elem = $("<div>").addClass("widget-content");
let out_data;
const callback = (data) => {
out_data = data;
};
const opts = {
elem: widget_elem,
$elem: $widget_elem,
callback,
message: {
sender_id: me.user_id,
@@ -342,59 +342,59 @@ run_test("activate own poll", ({mock_template}) => {
};
const set_widget_find_result = (selector) => {
const elem = $.create(selector);
widget_elem.set_find_results(selector, elem);
return elem;
const $elem = $.create(selector);
$widget_elem.set_find_results(selector, $elem);
return $elem;
};
set_widget_find_result("button.poll-option");
const poll_option_input = set_widget_find_result("input.poll-option");
const widget_option_container = set_widget_find_result("ul.poll-widget");
const $poll_option_input = set_widget_find_result("input.poll-option");
const $widget_option_container = set_widget_find_result("ul.poll-widget");
const poll_question_submit = set_widget_find_result("button.poll-question-check");
const poll_edit_question = set_widget_find_result(".poll-edit-question");
const poll_question_input = set_widget_find_result("input.poll-question");
const poll_question_header = set_widget_find_result(".poll-question-header");
const poll_question_container = set_widget_find_result(".poll-question-bar");
const poll_option_container = set_widget_find_result(".poll-option-bar");
const $poll_question_submit = set_widget_find_result("button.poll-question-check");
const $poll_edit_question = set_widget_find_result(".poll-edit-question");
const $poll_question_input = set_widget_find_result("input.poll-question");
const $poll_question_header = set_widget_find_result(".poll-question-header");
const $poll_question_container = set_widget_find_result(".poll-question-bar");
const $poll_option_container = set_widget_find_result(".poll-option-bar");
set_widget_find_result("button.poll-vote");
const poll_please_wait = set_widget_find_result(".poll-please-wait");
const poll_author_help = set_widget_find_result(".poll-author-help");
const $poll_please_wait = set_widget_find_result(".poll-please-wait");
const $poll_author_help = set_widget_find_result(".poll-author-help");
set_widget_find_result("button.poll-question-remove");
function assert_visibility() {
assert.ok(poll_option_container.visible());
assert.ok(poll_question_header.visible());
assert.ok(!poll_question_container.visible());
assert.ok(poll_edit_question.visible());
assert.ok(!poll_please_wait.visible());
assert.ok(!poll_author_help.visible());
assert.ok($poll_option_container.visible());
assert.ok($poll_question_header.visible());
assert.ok(!$poll_question_container.visible());
assert.ok($poll_edit_question.visible());
assert.ok(!$poll_please_wait.visible());
assert.ok(!$poll_author_help.visible());
}
poll_widget.activate(opts);
assert_visibility();
assert.ok(!poll_question_submit.visible());
assert.ok(!$poll_question_submit.visible());
assert.equal(widget_elem.html(), "widgets/poll_widget");
assert.equal(widget_option_container.html(), "widgets/poll_widget_results");
assert.equal(poll_question_header.text(), "Where to go?");
assert.equal($widget_elem.html(), "widgets/poll_widget");
assert.equal($widget_option_container.html(), "widgets/poll_widget_results");
assert.equal($poll_question_header.text(), "Where to go?");
{
/* Testing data sent to server on editing question */
poll_question_input.val("Is it new?");
$poll_question_input.val("Is it new?");
out_data = undefined;
poll_question_submit.trigger("click");
$poll_question_submit.trigger("click");
assert.deepEqual(out_data, {type: "question", question: "Is it new?"});
assert_visibility();
assert.ok(poll_question_submit.visible());
assert.ok($poll_question_submit.visible());
poll_option_input.val("");
$poll_option_input.val("");
out_data = undefined;
poll_question_submit.trigger("click");
$poll_question_submit.trigger("click");
assert.deepEqual(out_data, undefined);
}
});

View File

@@ -146,9 +146,9 @@ test_ui("sender_hover", ({override, mock_template}) => {
assert.equal(msg_id, message.id);
};
const target = $.create("click target");
const $target = $.create("click target");
target.closest = (sel) => {
$target.closest = (sel) => {
assert.equal(sel, ".message_row");
return {};
};
@@ -173,6 +173,7 @@ test_ui("sender_hover", ({override, mock_template}) => {
can_set_away: false,
can_revoke_away: false,
can_mute: true,
can_manage_user: false,
can_unmute: false,
user_full_name: "Alice Smith",
user_email: "alice@example.com",
@@ -198,14 +199,13 @@ test_ui("sender_hover", ({override, mock_template}) => {
user_mention_syntax: "@**Alice Smith**",
date_joined: undefined,
spectator_view: false,
show_manage_user_option: false,
});
return "content-html";
});
$.create(".user_popover_email", {children: []});
const image_stubber = make_image_stubber();
handler.call(target, e);
handler.call($target, e);
const avatar_img = image_stubber.get(0);
assert.equal(avatar_img.src.toString(), "/avatar/42/medium");
@@ -216,7 +216,7 @@ test_ui("sender_hover", ({override, mock_template}) => {
test_ui("actions_popover", ({override, override_rewire, mock_template}) => {
override($.fn, "popover", noop);
const target = $.create("click target");
const $target = $.create("click target");
const handler = $("#main_div").get_on_handler("click", ".actions_hover");
@@ -241,7 +241,7 @@ test_ui("actions_popover", ({override, override_rewire, mock_template}) => {
override_rewire(message_edit, "get_editability", () => 4);
target.closest = (sel) => {
$target.closest = (sel) => {
assert.equal(sel, ".message_row");
return {
toggleClass: noop,
@@ -257,5 +257,5 @@ test_ui("actions_popover", ({override, override_rewire, mock_template}) => {
return "actions-content";
});
handler.call(target, e);
handler.call($target, e);
});

View File

@@ -361,46 +361,46 @@ test("sending", ({override, override_rewire}) => {
});
test("set_reaction_count", () => {
const count_element = $.create("count-stub");
const reaction_element = $.create("reaction-stub");
const $count_element = $.create("count-stub");
const $reaction_element = $.create("reaction-stub");
reaction_element.set_find_results(".message_reaction_count", count_element);
$reaction_element.set_find_results(".message_reaction_count", $count_element);
reactions.set_reaction_count(reaction_element, 5);
reactions.set_reaction_count($reaction_element, 5);
assert.equal(count_element.text(), "5");
assert.equal($count_element.text(), "5");
});
test("find_reaction", ({override_rewire}) => {
const message_id = 99;
const local_id = "unicode_emoji,1f44b";
const reaction_section = $.create("section-stub");
const $reaction_section = $.create("section-stub");
const reaction_stub = "reaction-stub";
reaction_section.set_find_results(
$reaction_section.set_find_results(
`[data-reaction-id='${CSS.escape(local_id)}']`,
reaction_stub,
);
override_rewire(reactions, "get_reaction_section", (arg) => {
assert.equal(arg, message_id);
return reaction_section;
return $reaction_section;
});
assert.equal(reactions.find_reaction(message_id, local_id), reaction_stub);
});
test("get_reaction_section", () => {
const message_table = $.create(".message_table");
const message_row = $.create("some-message-row");
const message_reactions = $.create("our-reactions-section");
const $message_table = $.create(".message_table");
const $message_row = $.create("some-message-row");
const $message_reactions = $.create("our-reactions-section");
message_table.set_find_results(`[zid='${CSS.escape(555)}']`, message_row);
message_row.set_find_results(".message_reactions", message_reactions);
$message_table.set_find_results(`[zid='${CSS.escape(555)}']`, $message_row);
$message_row.set_find_results(".message_reactions", $message_reactions);
const section = reactions.get_reaction_section(555);
const $section = reactions.get_reaction_section(555);
assert.equal(section, message_reactions);
assert.equal($section, $message_reactions);
});
test("emoji_reaction_title", ({override}) => {
@@ -599,14 +599,14 @@ test("view.insert_new_reaction (me w/unicode emoji)", ({override_rewire, mock_te
user_id: alice.user_id,
};
const message_reactions = $.create("our-reactions");
const $message_reactions = $.create("our-reactions");
override_rewire(reactions, "get_reaction_section", (message_id) => {
assert.equal(message_id, opts.message_id);
return message_reactions;
return $message_reactions;
});
message_reactions.find = (selector) => {
$message_reactions.find = (selector) => {
assert.equal(selector, ".reaction_button");
return "reaction-button-stub";
};
@@ -624,11 +624,11 @@ test("view.insert_new_reaction (me w/unicode emoji)", ({override_rewire, mock_te
reaction_type: opts.reaction_type,
is_realm_emoji: false,
});
return "<new reaction html>";
return "<new-reaction-stub>";
});
let insert_called;
$("<new reaction html>").insertBefore = (element) => {
$("<new-reaction-stub>").insertBefore = (element) => {
assert.equal(element, "reaction-button-stub");
insert_called = true;
};
@@ -646,14 +646,14 @@ test("view.insert_new_reaction (them w/zulip emoji)", ({override_rewire, mock_te
user_id: bob.user_id,
};
const message_reactions = $.create("our-reactions");
const $message_reactions = $.create("our-reactions");
override_rewire(reactions, "get_reaction_section", (message_id) => {
assert.equal(message_id, opts.message_id);
return message_reactions;
return $message_reactions;
});
message_reactions.find = (selector) => {
$message_reactions.find = (selector) => {
assert.equal(selector, ".reaction_button");
return "reaction-button-stub";
};
@@ -673,11 +673,11 @@ test("view.insert_new_reaction (them w/zulip emoji)", ({override_rewire, mock_te
still_url: undefined,
reaction_type: opts.reaction_type,
});
return "<new reaction html>";
return "<new-reaction-stub>";
});
let insert_called;
$("<new reaction html>").insertBefore = (element) => {
$("<new-reaction-stub>").insertBefore = (element) => {
assert.equal(element, "reaction-button-stub");
insert_called = true;
};
@@ -696,24 +696,24 @@ test("view.update_existing_reaction (me)", ({override_rewire}) => {
user_list: [alice.user_id, bob.user_id],
};
const our_reaction = $.create("our-reaction-stub");
const $our_reaction = $.create("our-reaction-stub");
override_rewire(reactions, "find_reaction", (message_id, local_id) => {
assert.equal(message_id, opts.message_id);
assert.equal(local_id, "unicode_emoji,1f3b1");
return our_reaction;
return $our_reaction;
});
override_rewire(reactions, "set_reaction_count", (reaction, count) => {
assert.equal(reaction, our_reaction);
assert.equal(reaction, $our_reaction);
assert.equal(count, 2);
});
reactions.view.update_existing_reaction(opts);
assert.ok(our_reaction.hasClass("reacted"));
assert.ok($our_reaction.hasClass("reacted"));
assert.equal(
our_reaction.attr("aria-label"),
$our_reaction.attr("aria-label"),
"translated: You (click to remove) and Bob van Roberts reacted with :8ball:",
);
});
@@ -728,24 +728,24 @@ test("view.update_existing_reaction (them)", ({override_rewire}) => {
user_list: [alice.user_id, bob.user_id, cali.user_id, alexus.user_id],
};
const our_reaction = $.create("our-reaction-stub");
const $our_reaction = $.create("our-reaction-stub");
override_rewire(reactions, "find_reaction", (message_id, local_id) => {
assert.equal(message_id, opts.message_id);
assert.equal(local_id, "unicode_emoji,1f3b1");
return our_reaction;
return $our_reaction;
});
override_rewire(reactions, "set_reaction_count", (reaction, count) => {
assert.equal(reaction, our_reaction);
assert.equal(reaction, $our_reaction);
assert.equal(count, 4);
});
reactions.view.update_existing_reaction(opts);
assert.ok(!our_reaction.hasClass("reacted"));
assert.ok(!$our_reaction.hasClass("reacted"));
assert.equal(
our_reaction.attr("aria-label"),
$our_reaction.attr("aria-label"),
"translated: You (click to remove), Bob van Roberts, Cali and Alexus reacted with :8ball:",
);
});
@@ -760,25 +760,25 @@ test("view.remove_reaction (me)", ({override_rewire}) => {
user_list: [bob.user_id, cali.user_id],
};
const our_reaction = $.create("our-reaction-stub");
our_reaction.addClass("reacted");
const $our_reaction = $.create("our-reaction-stub");
$our_reaction.addClass("reacted");
override_rewire(reactions, "find_reaction", (message_id, local_id) => {
assert.equal(message_id, opts.message_id);
assert.equal(local_id, "unicode_emoji,1f3b1");
return our_reaction;
return $our_reaction;
});
override_rewire(reactions, "set_reaction_count", (reaction, count) => {
assert.equal(reaction, our_reaction);
assert.equal(reaction, $our_reaction);
assert.equal(count, 2);
});
reactions.view.remove_reaction(opts);
assert.ok(!our_reaction.hasClass("reacted"));
assert.ok(!$our_reaction.hasClass("reacted"));
assert.equal(
our_reaction.attr("aria-label"),
$our_reaction.attr("aria-label"),
"translated: Bob van Roberts and Cali reacted with :8ball:",
);
});
@@ -793,26 +793,26 @@ test("view.remove_reaction (them)", ({override_rewire}) => {
user_list: [alice.user_id],
};
const our_reaction = $.create("our-reaction-stub");
our_reaction.addClass("reacted");
const $our_reaction = $.create("our-reaction-stub");
$our_reaction.addClass("reacted");
override_rewire(reactions, "find_reaction", (message_id, local_id) => {
assert.equal(message_id, opts.message_id);
assert.equal(local_id, "unicode_emoji,1f3b1");
return our_reaction;
return $our_reaction;
});
override_rewire(reactions, "set_reaction_count", (reaction, count) => {
assert.equal(reaction, our_reaction);
assert.equal(reaction, $our_reaction);
assert.equal(count, 1);
});
our_reaction.addClass("reacted");
$our_reaction.addClass("reacted");
reactions.view.remove_reaction(opts);
assert.ok(our_reaction.hasClass("reacted"));
assert.ok($our_reaction.hasClass("reacted"));
assert.equal(
our_reaction.attr("aria-label"),
$our_reaction.attr("aria-label"),
"translated: You (click to remove) reacted with :8ball:",
);
});
@@ -827,16 +827,16 @@ test("view.remove_reaction (last person)", ({override_rewire}) => {
user_list: [],
};
const our_reaction = $.create("our-reaction-stub");
const $our_reaction = $.create("our-reaction-stub");
override_rewire(reactions, "find_reaction", (message_id, local_id) => {
assert.equal(message_id, opts.message_id);
assert.equal(local_id, "unicode_emoji,1f3b1");
return our_reaction;
return $our_reaction;
});
let removed;
our_reaction.remove = () => {
$our_reaction.remove = () => {
removed = true;
};
reactions.view.remove_reaction(opts);

View File

@@ -97,9 +97,10 @@ mock_esm("../../static/js/muted_topics", {
return false;
},
});
mock_esm("../../static/js/narrow", {
const narrow = mock_esm("../../static/js/narrow", {
set_narrow_title: noop,
hide_mark_as_read_turned_off_banner: noop,
has_shown_message_list_view: true,
});
mock_esm("../../static/js/recent_senders", {
get_topic_recent_senders: () => [1, 2],
@@ -115,7 +116,7 @@ mock_esm("../../static/js/stream_list", {
handle_narrow_deactivated: noop,
});
mock_esm("../../static/js/timerender", {
format_time_modern: () => "Just now",
last_seen_status_from_date: () => "Just now",
get_full_datetime: () => "date at time",
});
mock_esm("../../static/js/sub_store", {
@@ -323,9 +324,9 @@ function stub_out_filter_buttons() {
// See show_selected_filters() and set_filter() in the
// implementation.
for (const filter of ["all", "unread", "muted", "participated"]) {
const stub = $.create(`filter-${filter}-stub`);
const $stub = $.create(`filter-${filter}-stub`);
const selector = `[data-filter="${filter}"]`;
$("#recent_topics_filter_buttons").set_find_results(selector, stub);
$("#recent_topics_filter_buttons").set_find_results(selector, $stub);
}
}
@@ -338,7 +339,9 @@ function test(label, f) {
});
}
test("test_recent_topics_show", ({mock_template}) => {
test("test_recent_topics_show", ({mock_template, override}) => {
override(narrow, "save_pre_narrow_offset_for_reload", () => {});
// Note: unread count and urls are fake,
// since they are generated in external libraries
// and are not to be tested here.

View File

@@ -104,12 +104,12 @@ const get_content_element = () => {
};
run_test("misc_helpers", () => {
const elem = $.create("user-mention");
rm.set_name_in_mention_element(elem, "Aaron");
assert.equal(elem.text(), "@Aaron");
elem.addClass("silent");
rm.set_name_in_mention_element(elem, "Aaron, but silent");
assert.equal(elem.text(), "Aaron, but silent");
const $elem = $.create("user-mention");
rm.set_name_in_mention_element($elem, "Aaron");
assert.equal($elem.text(), "@Aaron");
$elem.addClass("silent");
rm.set_name_in_mention_element($elem, "Aaron, but silent");
assert.equal($elem.text(), "Aaron, but silent");
});
run_test("user-mention", () => {
@@ -423,8 +423,8 @@ function test_code_playground(mock_template, viewing_code) {
return {
prepends,
copy_code: $copy_code_button,
view_code: $view_code_in_playground,
$copy_code: $copy_code_button,
$view_code: $view_code_in_playground,
};
}
@@ -434,12 +434,12 @@ run_test("code playground none", ({override, mock_template}) => {
return undefined;
});
const {prepends, copy_code, view_code} = test_code_playground(mock_template, false);
assert.deepEqual(prepends, [copy_code]);
const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, false);
assert.deepEqual(prepends, [$copy_code]);
assert_clipboard_setup();
assert.equal(view_code.attr("data-tippy-content"), undefined);
assert.equal(view_code.attr("aria-label"), undefined);
assert.equal($view_code.attr("data-tippy-content"), undefined);
assert.equal($view_code.attr("aria-label"), undefined);
});
run_test("code playground single", ({override, mock_template}) => {
@@ -448,16 +448,16 @@ run_test("code playground single", ({override, mock_template}) => {
return [{name: "Some Javascript Playground"}];
});
const {prepends, copy_code, view_code} = test_code_playground(mock_template, true);
assert.deepEqual(prepends, [view_code, copy_code]);
const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, true);
assert.deepEqual(prepends, [$view_code, $copy_code]);
assert_clipboard_setup();
assert.equal(
view_code.attr("data-tippy-content"),
$view_code.attr("data-tippy-content"),
"translated: View in Some Javascript Playground",
);
assert.equal(view_code.attr("aria-label"), "translated: View in Some Javascript Playground");
assert.equal(view_code.attr("aria-haspopup"), undefined);
assert.equal($view_code.attr("aria-label"), "translated: View in Some Javascript Playground");
assert.equal($view_code.attr("aria-haspopup"), undefined);
});
run_test("code playground multiple", ({override, mock_template}) => {
@@ -466,13 +466,13 @@ run_test("code playground multiple", ({override, mock_template}) => {
return ["whatever", "whatever"];
});
const {prepends, copy_code, view_code} = test_code_playground(mock_template, true);
assert.deepEqual(prepends, [view_code, copy_code]);
const {prepends, $copy_code, $view_code} = test_code_playground(mock_template, true);
assert.deepEqual(prepends, [$view_code, $copy_code]);
assert_clipboard_setup();
assert.equal(view_code.attr("data-tippy-content"), "translated: View in playground");
assert.equal(view_code.attr("aria-label"), "translated: View in playground");
assert.equal(view_code.attr("aria-haspopup"), "true");
assert.equal($view_code.attr("data-tippy-content"), "translated: View in playground");
assert.equal($view_code.attr("aria-label"), "translated: View in playground");
assert.equal($view_code.attr("aria-haspopup"), "true");
});
run_test("rtl", () => {

View File

@@ -129,20 +129,20 @@ run_test("get_direction", () => {
});
run_test("set_rtl_class_for_textarea rtl", () => {
const textarea = $.create("some-textarea");
assert.ok(!textarea.hasClass("rtl"));
const $textarea = $.create("some-textarea");
assert.ok(!$textarea.hasClass("rtl"));
const text = "```quote\nمرحبا";
textarea.val(text);
rtl.set_rtl_class_for_textarea(textarea);
assert.ok(textarea.hasClass("rtl"));
$textarea.val(text);
rtl.set_rtl_class_for_textarea($textarea);
assert.ok($textarea.hasClass("rtl"));
});
run_test("set_rtl_class_for_textarea ltr", () => {
const textarea = $.create("some-textarea");
textarea.addClass("rtl");
assert.ok(textarea.hasClass("rtl"));
const $textarea = $.create("some-textarea");
$textarea.addClass("rtl");
assert.ok($textarea.hasClass("rtl"));
const text = "```quote\nEnglish text";
textarea.val(text);
rtl.set_rtl_class_for_textarea(textarea);
assert.ok(!textarea.hasClass("rtl"));
$textarea.val(text);
rtl.set_rtl_class_for_textarea($textarea);
assert.ok(!$textarea.hasClass("rtl"));
});

View File

@@ -89,7 +89,7 @@ run_test("scroll_delta", () => {
});
run_test("scroll_element_into_container", () => {
const container = (function () {
const $container = (function () {
let top = 3;
return {
height: () => 100,
@@ -103,21 +103,21 @@ run_test("scroll_element_into_container", () => {
};
})();
const elem1 = {
const $elem1 = {
innerHeight: () => 25,
position: () => ({
top: 0,
}),
};
scroll_util.scroll_element_into_container(elem1, container);
assert.equal(container.scrollTop(), 3);
scroll_util.scroll_element_into_container($elem1, $container);
assert.equal($container.scrollTop(), 3);
const elem2 = {
const $elem2 = {
innerHeight: () => 15,
position: () => ({
top: 250,
}),
};
scroll_util.scroll_element_into_container(elem2, container);
assert.equal(container.scrollTop(), 250 - 100 + 3 + 15);
scroll_util.scroll_element_into_container($elem2, $container);
assert.equal($container.scrollTop(), 250 - 100 + 3 + 15);
});

View File

@@ -51,50 +51,50 @@ test("clear_search_form", () => {
});
test("update_button_visibility", () => {
const search_query = $("#search_query");
const search_button = $(".search_button");
const $search_query = $("#search_query");
const $search_button = $(".search_button");
search_query.is = () => false;
search_query.val("");
$search_query.is = () => false;
$search_query.val("");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(search_button.prop("disabled"));
assert.ok($search_button.prop("disabled"));
search_query.is = () => true;
search_query.val("");
$search_query.is = () => true;
$search_query.val("");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
search_query.is = () => false;
search_query.val("Test search term");
$search_query.is = () => false;
$search_query.val("Test search term");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
search_query.is = () => false;
search_query.val("");
$search_query.is = () => false;
$search_query.val("");
narrow_state.active = () => true;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
});
test("initialize", () => {
const search_query_box = $("#search_query");
const searchbox_form = $("#searchbox_form");
const search_button = $(".search_button");
const searchbox = $("#searchbox");
const $search_query_box = $("#search_query");
const $searchbox_form = $("#searchbox_form");
const $search_button = $(".search_button");
const $searchbox = $("#searchbox");
search_query_box[0] = "stub";
$search_query_box[0] = "stub";
search_pill.get_search_string_for_current_filter = () => "is:starred";
search_suggestion.max_num_of_search_results = 99;
search_query_box.typeahead = (opts) => {
$search_query_box.typeahead = (opts) => {
assert.equal(opts.fixed, true);
assert.equal(opts.items, 99);
assert.equal(opts.naturalSearch, true);
@@ -144,7 +144,7 @@ test("initialize", () => {
let operators;
let is_blurred;
let is_append_search_string_called;
search_query_box.on("blur", () => {
$search_query_box.on("blur", () => {
is_blurred = true;
});
search_pill.append_search_string = () => {
@@ -154,7 +154,7 @@ test("initialize", () => {
const _setup = (search_box_val) => {
is_blurred = false;
is_append_search_string_called = false;
search_query_box.val(search_box_val);
$search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
@@ -199,34 +199,35 @@ test("initialize", () => {
assert.ok(!is_blurred);
assert.ok(is_append_search_string_called);
search_query_box.off("blur");
$search_query_box.off("blur");
}
};
search.initialize();
const search_pill_stub = $.create(".pill");
search_pill_stub.closest = () => ({data: noop});
const $search_pill_stub = $.create(".pill");
$search_pill_stub.closest = () => ({data: noop});
const stub_event = {
relatedTarget: search_pill_stub,
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $search_pill_stub,
};
search_query_box.val("test string");
$search_query_box.val("test string");
narrow_state.search_string = () => "ver";
search_query_box.trigger(new $.Event("blur", stub_event));
assert.equal(search_query_box.val(), "test string");
$search_query_box.trigger(new $.Event("blur", stub_event));
assert.equal($search_query_box.val(), "test string");
let css_args;
searchbox.css = (args) => {
$searchbox.css = (args) => {
css_args = args;
};
searchbox.trigger("focusout");
$searchbox.trigger("focusout");
assert.deepEqual(css_args, {"box-shadow": "unset"});
search.__Rewire__("is_using_input_method", false);
searchbox_form.trigger("compositionend");
$searchbox_form.trigger("compositionend");
assert.ok(search.is_using_input_method);
const keydown = searchbox_form.get_on_handler("keydown");
const keydown = $searchbox_form.get_on_handler("keydown");
let default_prevented = false;
let ev = {
type: "keydown",
@@ -235,7 +236,7 @@ test("initialize", () => {
default_prevented = true;
},
};
search_query_box.is = () => false;
$search_query_box.is = () => false;
assert.equal(keydown(ev), undefined);
assert.ok(!default_prevented);
@@ -244,22 +245,22 @@ test("initialize", () => {
assert.ok(!default_prevented);
ev.key = "Enter";
search_query_box.is = () => true;
$search_query_box.is = () => true;
assert.equal(keydown(ev), undefined);
assert.ok(default_prevented);
let operators;
let is_blurred;
narrow_state.active = () => false;
search_query_box.off("blur");
search_query_box.on("blur", () => {
$search_query_box.off("blur");
$search_query_box.on("blur", () => {
is_blurred = true;
});
const _setup = (search_box_val) => {
is_blurred = false;
search_button.prop("disabled", false);
search_query_box.val(search_box_val);
$search_button.prop("disabled", false);
$search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
@@ -284,41 +285,41 @@ test("initialize", () => {
type: "keyup",
which: 15,
};
search_query_box.is = () => false;
searchbox_form.trigger(ev);
$search_query_box.is = () => false;
$searchbox_form.trigger(ev);
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
ev.key = "Enter";
search_query_box.is = () => false;
searchbox_form.trigger(ev);
$search_query_box.is = () => false;
$searchbox_form.trigger(ev);
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
ev.key = "Enter";
search_query_box.is = () => true;
searchbox_form.trigger(ev);
$search_query_box.is = () => true;
$searchbox_form.trigger(ev);
assert.ok(is_blurred);
_setup("ver");
search.__Rewire__("is_using_input_method", true);
searchbox_form.trigger(ev);
$searchbox_form.trigger(ev);
// No change on Enter keyup event when using input tool
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
_setup("ver");
ev.key = "Enter";
search_query_box.is = () => true;
searchbox_form.trigger(ev);
$search_query_box.is = () => true;
$searchbox_form.trigger(ev);
assert.ok(is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
search_button.prop("disabled", true);
search_query_box.trigger("focus");
assert.ok(!search_button.prop("disabled"));
$search_button.prop("disabled", true);
$search_query_box.trigger("focus");
assert.ok(!$search_button.prop("disabled"));
});
test("initiate_search", () => {
@@ -345,9 +346,9 @@ test("initiate_search", () => {
$("#search_query")[0] = "stub";
const searchbox = $("#searchbox");
const $searchbox = $("#searchbox");
let css_args;
searchbox.css = (args) => {
$searchbox.css = (args) => {
css_args = args;
};

View File

@@ -28,46 +28,58 @@ set_global("setTimeout", (func) => func());
const search = zrequire("search");
run_test("clear_search_form", () => {
$("#search_query").val("noise");
$("#search_query").trigger("focus");
$(".search_button").prop("disabled", false);
search.clear_search_form();
assert.equal($("#search_query").is_focused(), false);
assert.equal($("#search_query").val(), "");
assert.equal($(".search_button").prop("disabled"), true);
});
run_test("update_button_visibility", () => {
const search_query = $("#search_query");
const search_button = $(".search_button");
const $search_query = $("#search_query");
const $search_button = $(".search_button");
search_query.is = () => false;
search_query.val("");
$search_query.is = () => false;
$search_query.val("");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(search_button.prop("disabled"));
assert.ok($search_button.prop("disabled"));
search_query.is = () => true;
search_query.val("");
$search_query.is = () => true;
$search_query.val("");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
search_query.is = () => false;
search_query.val("Test search term");
$search_query.is = () => false;
$search_query.val("Test search term");
narrow_state.active = () => false;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
search_query.is = () => false;
search_query.val("");
$search_query.is = () => false;
$search_query.val("");
narrow_state.active = () => true;
search_button.prop("disabled", true);
$search_button.prop("disabled", true);
search.update_button_visibility();
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
});
run_test("initialize", () => {
const search_query_box = $("#search_query");
const searchbox_form = $("#searchbox_form");
const search_button = $(".search_button");
const $search_query_box = $("#search_query");
const $searchbox_form = $("#searchbox_form");
const $search_button = $(".search_button");
search_suggestion.max_num_of_search_results = 999;
search_query_box.typeahead = (opts) => {
$search_query_box.typeahead = (opts) => {
assert.equal(opts.fixed, true);
assert.equal(opts.items, 999);
assert.equal(opts.naturalSearch, true);
@@ -115,13 +127,13 @@ run_test("initialize", () => {
{
let operators;
let is_blurred;
search_query_box.on("blur", () => {
$search_query_box.on("blur", () => {
is_blurred = true;
});
/* Test updater */
const _setup = (search_box_val) => {
is_blurred = false;
search_query_box.val(search_box_val);
$search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
@@ -159,26 +171,26 @@ run_test("initialize", () => {
assert.equal(opts.updater("stream:Verona"), "stream:Verona");
assert.ok(!is_blurred);
search_query_box.off("blur");
$search_query_box.off("blur");
}
};
search.initialize();
search_button.prop("disabled", true);
search_query_box.trigger("focus");
assert.ok(!search_button.prop("disabled"));
$search_button.prop("disabled", true);
$search_query_box.trigger("focus");
assert.ok(!$search_button.prop("disabled"));
search_query_box.val("test string");
$search_query_box.val("test string");
narrow_state.search_string = () => "ver";
search_query_box.trigger("blur");
assert.equal(search_query_box.val(), "test string");
$search_query_box.trigger("blur");
assert.equal($search_query_box.val(), "test string");
search.__Rewire__("is_using_input_method", false);
searchbox_form.trigger("compositionend");
$searchbox_form.trigger("compositionend");
assert.ok(search.is_using_input_method);
const keydown = searchbox_form.get_on_handler("keydown");
const keydown = $searchbox_form.get_on_handler("keydown");
let default_prevented = false;
let ev = {
type: "keydown",
@@ -187,7 +199,7 @@ run_test("initialize", () => {
default_prevented = true;
},
};
search_query_box.is = () => false;
$search_query_box.is = () => false;
assert.equal(keydown(ev), undefined);
assert.ok(!default_prevented);
@@ -196,7 +208,7 @@ run_test("initialize", () => {
assert.ok(!default_prevented);
ev.key = "Enter";
search_query_box.is = () => true;
$search_query_box.is = () => true;
assert.equal(keydown(ev), undefined);
assert.ok(default_prevented);
@@ -206,15 +218,15 @@ run_test("initialize", () => {
let operators;
let is_blurred;
narrow_state.active = () => false;
search_query_box.off("blur");
search_query_box.on("blur", () => {
$search_query_box.off("blur");
$search_query_box.on("blur", () => {
is_blurred = true;
});
const _setup = (search_box_val) => {
is_blurred = false;
search_button.prop("disabled", false);
search_query_box.val(search_box_val);
$search_button.prop("disabled", false);
$search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
@@ -235,37 +247,37 @@ run_test("initialize", () => {
_setup("");
ev.key = "a";
search_query_box.is = () => false;
searchbox_form.trigger(ev);
$search_query_box.is = () => false;
$searchbox_form.trigger(ev);
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
ev.key = "Enter";
search_query_box.is = () => false;
searchbox_form.trigger(ev);
$search_query_box.is = () => false;
$searchbox_form.trigger(ev);
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
ev.key = "Enter";
search_query_box.is = () => true;
searchbox_form.trigger(ev);
$search_query_box.is = () => true;
$searchbox_form.trigger(ev);
assert.ok(is_blurred);
_setup("ver");
search.__Rewire__("is_using_input_method", true);
searchbox_form.trigger(ev);
$searchbox_form.trigger(ev);
// No change on Enter keyup event when using input tool
assert.ok(!is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
_setup("ver");
ev.key = "Enter";
search_query_box.is = () => true;
searchbox_form.trigger(ev);
$search_query_box.is = () => true;
$searchbox_form.trigger(ev);
assert.ok(is_blurred);
assert.ok(!search_button.prop("disabled"));
assert.ok(!$search_button.prop("disabled"));
});
run_test("initiate_search", () => {

View File

@@ -2,7 +2,7 @@
const {strict: assert} = require("assert");
const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {mock_esm, with_field, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const {page_params} = require("../zjsunit/zpage_params");
@@ -93,6 +93,16 @@ test("basic_get_suggestions", ({override_rewire}) => {
assert.deepEqual(suggestions.strings, expected);
});
test("basic_get_suggestions_for_spectator", ({override_rewire}) => {
override_rewire(stream_data, "subscribed_streams", () => []);
page_params.is_spectator = true;
const query = "";
const suggestions = get_suggestions("", query);
assert.deepEqual(suggestions.strings, ["", "has:link", "has:image", "has:attachment"]);
page_params.is_spectator = false;
});
test("subset_suggestions", () => {
const query = "stream:Denmark topic:Hamlet shakespeare";
@@ -709,6 +719,49 @@ test("topic_suggestions", ({override, override_rewire}) => {
assert.deepEqual(suggestions.strings, expected);
});
test("topic_suggestions (limits)", () => {
let candidate_topics = [];
function assert_result(guess, expected_topics) {
assert.deepEqual(
search.get_topic_suggestions_from_candidates({candidate_topics, guess}),
expected_topics,
);
}
assert_result("", []);
assert_result("zzz", []);
candidate_topics = ["a", "b", "c"];
assert_result("", ["a", "b", "c"]);
assert_result("b", ["b"]);
assert_result("z", []);
candidate_topics = [
"a1",
"a2",
"b1",
"b2",
"a3",
"a4",
"a5",
"c1",
"a6",
"a7",
"a8",
"c2",
"a9",
"a10",
"a11",
"a12",
];
// We max out at 10 topics, so as not to overwhelm the user.
assert_result("", ["a1", "a2", "b1", "b2", "a3", "a4", "a5", "c1", "a6", "a7"]);
assert_result("a", ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"]);
assert_result("b", ["b1", "b2"]);
assert_result("z", []);
});
test("whitespace_glitch", ({override_rewire}) => {
const query = "stream:office "; // note trailing space
@@ -877,3 +930,58 @@ test("queries_with_spaces", ({override_rewire}) => {
expected = ["stream:offi", "stream:office"];
assert.deepEqual(suggestions.strings, expected);
});
function people_suggestion_setup() {
const ted = {
email: "ted@zulip.com",
user_id: 201,
full_name: "Ted Smith",
};
people.add_active_user(ted);
const bob = {
email: "bob@zulip.com",
user_id: 202,
full_name: "Bob Térry",
};
people.add_active_user(bob);
const alice = {
email: "alice@zulip.com",
user_id: 203,
full_name: "Alice Ignore",
};
people.add_active_user(alice);
}
test("people_suggestion (Admin only email visibility)", ({override_rewire}) => {
/* Suggestions when realm_email_address_visibility is set to admin
only */
override_rewire(narrow_state, "stream", () => {});
people_suggestion_setup();
const query = "te";
const suggestions = with_field(page_params, "is_admin", false, () =>
get_suggestions("", query),
);
const expected = [
"te",
"sender:bob@zulip.com",
"sender:ted@zulip.com",
"pm-with:bob@zulip.com", // bob térry
"pm-with:ted@zulip.com",
"group-pm-with:bob@zulip.com",
"group-pm-with:ted@zulip.com",
];
assert.deepEqual(suggestions.strings, expected);
const describe = (q) => suggestions.lookup_table.get(q).description;
assert.equal(
describe("pm-with:ted@zulip.com"),
"Private messages with <strong>Te</strong>d Smith",
);
assert.equal(describe("sender:ted@zulip.com"), "Sent by <strong>Te</strong>d Smith");
});

View File

@@ -91,32 +91,32 @@ test("generate_botserverrc_content", () => {
});
function test_create_bot_type_input_box_toggle(f) {
const create_payload_url = $("#create_payload_url");
const payload_url_inputbox = $("#payload_url_inputbox");
const config_inputbox = $("#config_inputbox");
const $create_payload_url = $("#create_payload_url");
const $payload_url_inputbox = $("#payload_url_inputbox");
const $config_inputbox = $("#config_inputbox");
const EMBEDDED_BOT_TYPE = "4";
const OUTGOING_WEBHOOK_BOT_TYPE = "3";
const GENERIC_BOT_TYPE = "1";
$("#create_bot_type :selected").val(EMBEDDED_BOT_TYPE);
f();
assert.ok(!create_payload_url.hasClass("required"));
assert.ok(!payload_url_inputbox.visible());
assert.ok(!$create_payload_url.hasClass("required"));
assert.ok(!$payload_url_inputbox.visible());
assert.ok($("#select_service_name").hasClass("required"));
assert.ok($("#service_name_list").visible());
assert.ok(config_inputbox.visible());
assert.ok($config_inputbox.visible());
$("#create_bot_type :selected").val(OUTGOING_WEBHOOK_BOT_TYPE);
f();
assert.ok(create_payload_url.hasClass("required"));
assert.ok(payload_url_inputbox.visible());
assert.ok(!config_inputbox.visible());
assert.ok($create_payload_url.hasClass("required"));
assert.ok($payload_url_inputbox.visible());
assert.ok(!$config_inputbox.visible());
$("#create_bot_type :selected").val(GENERIC_BOT_TYPE);
f();
assert.ok(!create_payload_url.hasClass("required"));
assert.ok(!payload_url_inputbox.visible());
assert.ok(!config_inputbox.visible());
assert.ok(!$create_payload_url.hasClass("required"));
assert.ok(!$payload_url_inputbox.visible());
assert.ok(!$config_inputbox.visible());
}
test("test tab clicks", ({override}) => {
@@ -125,10 +125,10 @@ test("test tab clicks", ({override}) => {
$("#create_bot_form").validate = () => {};
$("#config_inputbox").children = () => {
const mock_children = {
const $mock_children = {
hide: () => {},
};
return mock_children;
return $mock_children;
};
override(avatar, "build_bot_create_widget", () => {});
@@ -137,55 +137,55 @@ test("test tab clicks", ({override}) => {
test_create_bot_type_input_box_toggle(() => $("#create_bot_type").trigger("change"));
function click_on_tab(tab_elem) {
tab_elem.trigger("click");
function click_on_tab($tab_elem) {
$tab_elem.trigger("click");
}
const tabs = {
add: $("#bots_lists_navbar .add-a-new-bot-tab"),
active: $("#bots_lists_navbar .active-bots-tab"),
inactive: $("#bots_lists_navbar .inactive-bots-tab"),
$add: $("#bots_lists_navbar .add-a-new-bot-tab"),
$active: $("#bots_lists_navbar .active-bots-tab"),
$inactive: $("#bots_lists_navbar .inactive-bots-tab"),
};
$("#bots_lists_navbar .active").removeClass = (cls) => {
assert.equal(cls, "active");
for (const tab of Object.values(tabs)) {
tab.removeClass("active");
for (const $tab of Object.values(tabs)) {
$tab.removeClass("active");
}
};
const forms = {
add: $("#add-a-new-bot-form"),
active: $("#active_bots_list"),
inactive: $("#inactive_bots_list"),
$add: $("#add-a-new-bot-form"),
$active: $("#active_bots_list"),
$inactive: $("#inactive_bots_list"),
};
click_on_tab(tabs.add);
assert.ok(tabs.add.hasClass("active"));
assert.ok(!tabs.active.hasClass("active"));
assert.ok(!tabs.inactive.hasClass("active"));
click_on_tab(tabs.$add);
assert.ok(tabs.$add.hasClass("active"));
assert.ok(!tabs.$active.hasClass("active"));
assert.ok(!tabs.$inactive.hasClass("active"));
assert.ok(forms.add.visible());
assert.ok(!forms.active.visible());
assert.ok(!forms.inactive.visible());
assert.ok(forms.$add.visible());
assert.ok(!forms.$active.visible());
assert.ok(!forms.$inactive.visible());
click_on_tab(tabs.active);
assert.ok(!tabs.add.hasClass("active"));
assert.ok(tabs.active.hasClass("active"));
assert.ok(!tabs.inactive.hasClass("active"));
click_on_tab(tabs.$active);
assert.ok(!tabs.$add.hasClass("active"));
assert.ok(tabs.$active.hasClass("active"));
assert.ok(!tabs.$inactive.hasClass("active"));
assert.ok(!forms.add.visible());
assert.ok(forms.active.visible());
assert.ok(!forms.inactive.visible());
assert.ok(!forms.$add.visible());
assert.ok(forms.$active.visible());
assert.ok(!forms.$inactive.visible());
click_on_tab(tabs.inactive);
assert.ok(!tabs.add.hasClass("active"));
assert.ok(!tabs.active.hasClass("active"));
assert.ok(tabs.inactive.hasClass("active"));
click_on_tab(tabs.$inactive);
assert.ok(!tabs.$add.hasClass("active"));
assert.ok(!tabs.$active.hasClass("active"));
assert.ok(tabs.$inactive.hasClass("active"));
assert.ok(!forms.add.visible());
assert.ok(!forms.active.visible());
assert.ok(forms.inactive.visible());
assert.ok(!forms.$add.visible());
assert.ok(!forms.$active.visible());
assert.ok(forms.$inactive.visible());
});
test("can_create_new_bots", () => {

View File

@@ -51,15 +51,15 @@ run_test("settings", ({override_rewire}) => {
stopPropagation: noop,
};
const topic_fake_this = $.create("fake.settings-unmute-topic");
const topic_tr_html = $('tr[data-topic="js"]');
topic_fake_this.closest = (opts) => {
const $topic_fake_this = $.create("fake.settings-unmute-topic");
const $topic_tr_html = $('tr[data-topic="js"]');
$topic_fake_this.closest = (opts) => {
assert.equal(opts, "tr");
return topic_tr_html;
return $topic_tr_html;
};
let topic_data_called = 0;
topic_tr_html.attr = (opts) => {
$topic_tr_html.attr = (opts) => {
if (opts === "data-stream-id") {
topic_data_called += 1;
return frontend.stream_id;
@@ -77,7 +77,7 @@ run_test("settings", ({override_rewire}) => {
assert.equal(topic, "js");
unmute_topic_called = true;
};
topic_click_handler.call(topic_fake_this, event);
topic_click_handler.call($topic_fake_this, event);
assert.ok(unmute_topic_called);
assert.equal(topic_data_called, 2);
});

View File

@@ -42,15 +42,15 @@ run_test("settings", ({override_rewire}) => {
stopPropagation: noop,
};
const unmute_button = $.create("settings-unmute-user");
const fake_row = $('tr[data-user-id="5"]');
unmute_button.closest = (opts) => {
const $unmute_button = $.create("settings-unmute-user");
const $fake_row = $('tr[data-user-id="5"]');
$unmute_button.closest = (opts) => {
assert.equal(opts, "tr");
return fake_row;
return $fake_row;
};
let row_attribute_fetched = false;
fake_row.attr = (opts) => {
$fake_row.attr = (opts) => {
if (opts === "data-user-id") {
row_attribute_fetched += 1;
return "5";
@@ -64,7 +64,7 @@ run_test("settings", ({override_rewire}) => {
unmute_user_called = true;
};
unmute_click_handler.call(unmute_button, event);
unmute_click_handler.call($unmute_button, event);
assert.ok(unmute_user_called);
assert.ok(row_attribute_fetched);
});

View File

@@ -31,12 +31,12 @@ mock_esm("../../static/js/loading", {
destroy_indicator: noop,
});
mock_esm("../../static/js/ui_report", {
success(msg, elem) {
elem.val(msg);
success(msg, $elem) {
$elem.val(msg);
},
error(msg, xhr, elem) {
elem.val(msg);
error(msg, xhr, $elem) {
$elem.val(msg);
},
});
@@ -74,9 +74,9 @@ test("unloaded", () => {
});
function simulate_realm_domains_table() {
const tr_stub = $.create("realm-tr-stub");
$("#realm_domains_table tbody").set_find_results("tr", tr_stub);
tr_stub.remove = () => {};
const $tr_stub = $.create("realm-tr-stub");
$("#realm_domains_table tbody").set_find_results("tr", $tr_stub);
$tr_stub.remove = () => {};
let appended;
$("#realm_domains_table tbody").append = (html) => {
@@ -90,7 +90,7 @@ function simulate_realm_domains_table() {
}
function test_realms_domain_modal(override, add_realm_domain) {
const info = $(".realm_domains_info");
const $info = $(".realm_domains_info");
$("#add-realm-domain-widget").set_find_results(
".new-realm-domain",
@@ -117,46 +117,46 @@ function test_realms_domain_modal(override, add_realm_domain) {
assert.ok(posted);
success_callback();
assert.equal(info.val(), "translated HTML: Added successfully!");
assert.equal($info.val(), "translated HTML: Added successfully!");
error_callback({});
assert.equal(info.val(), "translated HTML: Failed");
assert.equal($info.val(), "translated HTML: Failed");
}
function createSaveButtons(subsection) {
const stub_save_button_header = $(`#org-${CSS.escape(subsection)}`);
const save_button_controls = $(".save-button-controls");
const stub_save_button = $(`#org-submit-${CSS.escape(subsection)}`);
const stub_discard_button = $(`#org-discard-${CSS.escape(subsection)}`);
const stub_save_button_text = $(".save-discard-widget-button-text");
stub_save_button_header.set_find_results(
const $stub_save_button_header = $(`#org-${CSS.escape(subsection)}`);
const $save_button_controls = $(".save-button-controls");
const $stub_save_button = $(`#org-submit-${CSS.escape(subsection)}`);
const $stub_discard_button = $(`#org-discard-${CSS.escape(subsection)}`);
const $stub_save_button_text = $(".save-discard-widget-button-text");
$stub_save_button_header.set_find_results(
".subsection-failed-status p",
$("<failed status element>"),
$("<failed-status-stub>"),
);
stub_save_button.closest = () => stub_save_button_header;
save_button_controls.set_find_results(".save-button", stub_save_button);
stub_save_button.set_find_results(".save-discard-widget-button-text", stub_save_button_text);
stub_save_button_header.set_find_results(".save-button-controls", save_button_controls);
stub_save_button_header.set_find_results(
$stub_save_button.closest = () => $stub_save_button_header;
$save_button_controls.set_find_results(".save-button", $stub_save_button);
$stub_save_button.set_find_results(".save-discard-widget-button-text", $stub_save_button_text);
$stub_save_button_header.set_find_results(".save-button-controls", $save_button_controls);
$stub_save_button_header.set_find_results(
".subsection-changes-discard button",
$(`#org-discard-${CSS.escape(subsection)}`),
);
save_button_controls.set_find_results(".discard-button", stub_discard_button);
$save_button_controls.set_find_results(".discard-button", $stub_discard_button);
const props = {};
props.hidden = false;
save_button_controls.fadeIn = () => {
$save_button_controls.fadeIn = () => {
props.hidden = false;
};
save_button_controls.fadeOut = () => {
$save_button_controls.fadeOut = () => {
props.hidden = true;
};
return {
props,
save_button: stub_save_button,
discard_button: stub_discard_button,
save_button_header: stub_save_button_header,
save_button_controls,
save_button_text: stub_save_button_text,
$save_button: $stub_save_button,
$discard_button: $stub_discard_button,
$save_button_header: $stub_save_button_header,
$save_button_controls,
$save_button_text: $stub_save_button_text,
};
}
@@ -193,55 +193,55 @@ function test_submit_settings_form(override, submit_form) {
let subsection = "other-permissions";
ev.currentTarget = `#org-submit-${CSS.escape(subsection)}`;
let stubs = createSaveButtons(subsection);
let save_button = stubs.save_button;
save_button.attr("id", `org-submit-${subsection}`);
save_button.replace = () => `${subsection}`;
let $save_button = stubs.$save_button;
$save_button.attr("id", `org-submit-${subsection}`);
$save_button.replace = () => `${subsection}`;
$("#id_realm_waiting_period_threshold").val(10);
const invite_to_stream_policy_elem = $("#id_realm_invite_to_stream_policy");
invite_to_stream_policy_elem.val("1");
invite_to_stream_policy_elem.attr("id", "id_realm_invite_to_stream_policy");
invite_to_stream_policy_elem.data = () => "number";
const $invite_to_stream_policy_elem = $("#id_realm_invite_to_stream_policy");
$invite_to_stream_policy_elem.val("1");
$invite_to_stream_policy_elem.attr("id", "id_realm_invite_to_stream_policy");
$invite_to_stream_policy_elem.data = () => "number";
const create_public_stream_policy_elem = $("#id_realm_create_public_stream_policy");
create_public_stream_policy_elem.val("2");
create_public_stream_policy_elem.attr("id", "id_realm_create_public_stream_policy");
create_public_stream_policy_elem.data = () => "number";
const $create_public_stream_policy_elem = $("#id_realm_create_public_stream_policy");
$create_public_stream_policy_elem.val("2");
$create_public_stream_policy_elem.attr("id", "id_realm_create_public_stream_policy");
$create_public_stream_policy_elem.data = () => "number";
const create_private_stream_policy_elem = $("#id_realm_create_private_stream_policy");
create_private_stream_policy_elem.val("2");
create_private_stream_policy_elem.attr("id", "id_realm_create_private_stream_policy");
create_private_stream_policy_elem.data = () => "number";
const $create_private_stream_policy_elem = $("#id_realm_create_private_stream_policy");
$create_private_stream_policy_elem.val("2");
$create_private_stream_policy_elem.attr("id", "id_realm_create_private_stream_policy");
$create_private_stream_policy_elem.data = () => "number";
const add_custom_emoji_policy_elem = $("#id_realm_add_custom_emoji_policy");
add_custom_emoji_policy_elem.val("1");
add_custom_emoji_policy_elem.attr("id", "id_realm_add_custom_emoji_policy");
add_custom_emoji_policy_elem.data = () => "number";
const $add_custom_emoji_policy_elem = $("#id_realm_add_custom_emoji_policy");
$add_custom_emoji_policy_elem.val("1");
$add_custom_emoji_policy_elem.attr("id", "id_realm_add_custom_emoji_policy");
$add_custom_emoji_policy_elem.data = () => "number";
const bot_creation_policy_elem = $("#id_realm_bot_creation_policy");
bot_creation_policy_elem.val("1");
bot_creation_policy_elem.attr("id", "id_realm_bot_creation_policy");
bot_creation_policy_elem.data = () => "number";
const email_address_visibility_elem = $("#id_realm_email_address_visibility");
email_address_visibility_elem.val("1");
email_address_visibility_elem.attr("id", "id_realm_email_address_visibility");
email_address_visibility_elem.data = () => "number";
const $bot_creation_policy_elem = $("#id_realm_bot_creation_policy");
$bot_creation_policy_elem.val("1");
$bot_creation_policy_elem.attr("id", "id_realm_bot_creation_policy");
$bot_creation_policy_elem.data = () => "number";
const $email_address_visibility_elem = $("#id_realm_email_address_visibility");
$email_address_visibility_elem.val("1");
$email_address_visibility_elem.attr("id", "id_realm_email_address_visibility");
$email_address_visibility_elem.data = () => "number";
const invite_to_realm_policy_elem = $("#id_realm_invite_to_realm_policy");
invite_to_realm_policy_elem.val("2");
invite_to_realm_policy_elem.attr("id", "id_realm_invite_to_realm_policy");
invite_to_realm_policy_elem.data = () => "number";
const $invite_to_realm_policy_elem = $("#id_realm_invite_to_realm_policy");
$invite_to_realm_policy_elem.val("2");
$invite_to_realm_policy_elem.attr("id", "id_realm_invite_to_realm_policy");
$invite_to_realm_policy_elem.data = () => "number";
let subsection_elem = $(`#org-${CSS.escape(subsection)}`);
subsection_elem.closest = () => subsection_elem;
subsection_elem.set_find_results(".prop-element", [
bot_creation_policy_elem,
email_address_visibility_elem,
add_custom_emoji_policy_elem,
create_public_stream_policy_elem,
create_private_stream_policy_elem,
invite_to_stream_policy_elem,
let $subsection_elem = $(`#org-${CSS.escape(subsection)}`);
$subsection_elem.closest = () => $subsection_elem;
$subsection_elem.set_find_results(".prop-element", [
$bot_creation_policy_elem,
$email_address_visibility_elem,
$add_custom_emoji_policy_elem,
$create_public_stream_policy_elem,
$create_private_stream_policy_elem,
$invite_to_stream_policy_elem,
]);
patched = false;
@@ -261,17 +261,17 @@ function test_submit_settings_form(override, submit_form) {
subsection = "user-defaults";
ev.currentTarget = `#org-submit-${CSS.escape(subsection)}`;
stubs = createSaveButtons(subsection);
save_button = stubs.save_button;
save_button.attr("id", `org-submit-${subsection}`);
$save_button = stubs.$save_button;
$save_button.attr("id", `org-submit-${subsection}`);
const realm_default_language_elem = $("#id_realm_default_language");
realm_default_language_elem.val("en");
realm_default_language_elem.attr("id", "id_realm_default_language");
realm_default_language_elem.data = () => "string";
const $realm_default_language_elem = $("#id_realm_default_language");
$realm_default_language_elem.val("en");
$realm_default_language_elem.attr("id", "id_realm_default_language");
$realm_default_language_elem.data = () => "string";
subsection_elem = $(`#org-${CSS.escape(subsection)}`);
subsection_elem.closest = () => subsection_elem;
subsection_elem.set_find_results(".prop-element", [realm_default_language_elem]);
$subsection_elem = $(`#org-${CSS.escape(subsection)}`);
$subsection_elem.closest = () => $subsection_elem;
$subsection_elem.set_find_results(".prop-element", [$realm_default_language_elem]);
submit_form(ev);
assert.ok(patched);
@@ -284,50 +284,50 @@ function test_submit_settings_form(override, submit_form) {
// Testing only once for since callback is same for all cases
success_callback();
assert.equal(stubs.props.hidden, true);
assert.equal(save_button.attr("data-status"), "saved");
assert.equal(stubs.save_button_text.text(), "translated: Saved");
assert.equal($save_button.attr("data-status"), "saved");
assert.equal(stubs.$save_button_text.text(), "translated: Saved");
}
function test_change_save_button_state() {
const {save_button_controls, save_button_text, save_button, discard_button, props} =
const {$save_button_controls, $save_button_text, $save_button, $discard_button, props} =
createSaveButtons("msg-editing");
save_button.attr("id", "org-submit-msg-editing");
$save_button.attr("id", "org-submit-msg-editing");
{
settings_org.change_save_button_state(save_button_controls, "unsaved");
assert.equal(save_button_text.text(), "translated: Save changes");
settings_org.change_save_button_state($save_button_controls, "unsaved");
assert.equal($save_button_text.text(), "translated: Save changes");
assert.equal(props.hidden, false);
assert.equal(save_button.attr("data-status"), "unsaved");
assert.equal(discard_button.visible(), true);
assert.equal($save_button.attr("data-status"), "unsaved");
assert.equal($discard_button.visible(), true);
}
{
settings_org.change_save_button_state(save_button_controls, "saved");
assert.equal(save_button_text.text(), "translated: Save changes");
settings_org.change_save_button_state($save_button_controls, "saved");
assert.equal($save_button_text.text(), "translated: Save changes");
assert.equal(props.hidden, true);
assert.equal(save_button.attr("data-status"), "");
assert.equal($save_button.attr("data-status"), "");
}
{
settings_org.change_save_button_state(save_button_controls, "saving");
assert.equal(save_button_text.text(), "translated: Saving");
assert.equal(save_button.attr("data-status"), "saving");
assert.equal(save_button.hasClass("saving"), true);
assert.equal(discard_button.visible(), false);
settings_org.change_save_button_state($save_button_controls, "saving");
assert.equal($save_button_text.text(), "translated: Saving");
assert.equal($save_button.attr("data-status"), "saving");
assert.equal($save_button.hasClass("saving"), true);
assert.equal($discard_button.visible(), false);
}
{
settings_org.change_save_button_state(save_button_controls, "discarded");
settings_org.change_save_button_state($save_button_controls, "discarded");
assert.equal(props.hidden, true);
}
{
settings_org.change_save_button_state(save_button_controls, "succeeded");
settings_org.change_save_button_state($save_button_controls, "succeeded");
assert.equal(props.hidden, true);
assert.equal(save_button.attr("data-status"), "saved");
assert.equal(save_button_text.text(), "translated: Saved");
assert.equal($save_button.attr("data-status"), "saved");
assert.equal($save_button_text.text(), "translated: Saved");
}
{
settings_org.change_save_button_state(save_button_controls, "failed");
settings_org.change_save_button_state($save_button_controls, "failed");
assert.equal(props.hidden, false);
assert.equal(save_button.attr("data-status"), "failed");
assert.equal(save_button_text.text(), "translated: Save changes");
assert.equal($save_button.attr("data-status"), "failed");
assert.equal($save_button_text.text(), "translated: Save changes");
}
}
@@ -358,8 +358,8 @@ function test_change_allow_subdomains(change_allow_subdomains) {
stopPropagation: noop,
};
const info = $(".realm_domains_info");
info.fadeOut = noop;
const $info = $(".realm_domains_info");
$info.fadeOut = noop;
const domain = "example.com";
let allow = true;
@@ -372,33 +372,33 @@ function test_change_allow_subdomains(change_allow_subdomains) {
error_callback = req.error;
};
const domain_obj = $.create("domain object");
domain_obj.text(domain);
const $domain_obj = $.create("domain object");
$domain_obj.text(domain);
const elem_obj = $.create("<elem html>");
const parents_obj = $.create("parents object");
const $elem_obj = $.create("<elem html>");
const $parents_obj = $.create("parents object");
elem_obj.set_parents_result("tr", parents_obj);
parents_obj.set_find_results(".domain", domain_obj);
elem_obj.prop("checked", allow);
$elem_obj.set_parents_result("tr", $parents_obj);
$parents_obj.set_find_results(".domain", $domain_obj);
$elem_obj.prop("checked", allow);
change_allow_subdomains.call(elem_obj, ev);
change_allow_subdomains.call($elem_obj, ev);
success_callback();
assert.equal(
info.val(),
$info.val(),
"translated HTML: Update successful: Subdomains allowed for example.com",
);
error_callback({});
assert.equal(info.val(), "translated HTML: Failed");
assert.equal($info.val(), "translated HTML: Failed");
allow = false;
elem_obj.prop("checked", allow);
change_allow_subdomains.call(elem_obj, ev);
$elem_obj.prop("checked", allow);
change_allow_subdomains.call($elem_obj, ev);
success_callback();
assert.equal(
info.val(),
$info.val(),
"translated HTML: Update successful: Subdomains no longer allowed for example.com",
);
}
@@ -431,9 +431,9 @@ function test_sync_realm_settings() {
{
/* Test invalid settings property sync */
const property_elem = $("#id_realm_invalid_settings_property");
property_elem.attr("id", "id_realm_invalid_settings_property");
property_elem.length = 1;
const $property_elem = $("#id_realm_invalid_settings_property");
$property_elem.attr("id", "id_realm_invalid_settings_property");
$property_elem.length = 1;
blueslip.expect(
"error",
@@ -443,9 +443,9 @@ function test_sync_realm_settings() {
}
function test_common_policy(property_name) {
const property_elem = $(`#id_realm_${CSS.escape(property_name)}`);
property_elem.length = 1;
property_elem.attr("id", `id_realm_${CSS.escape(property_name)}`);
const $property_elem = $(`#id_realm_${CSS.escape(property_name)}`);
$property_elem.length = 1;
$property_elem.attr("id", `id_realm_${CSS.escape(property_name)}`);
/* Each policy is initialized to 'by_members' and then all the values are tested
in the following order - by_admins_only, by_moderators_only, by_full_members,
@@ -453,14 +453,14 @@ function test_sync_realm_settings() {
page_params[`realm_${property_name}`] =
settings_config.common_policy_values.by_members.code;
property_elem.val(settings_config.common_policy_values.by_members.code);
$property_elem.val(settings_config.common_policy_values.by_members.code);
for (const policy_value of Array.from(
Object.values(settings_config.common_policy_values),
)) {
page_params[`realm_${property_name}`] = policy_value.code;
settings_org.sync_realm_settings(property_name);
assert.equal(property_elem.val(), policy_value.code);
assert.equal($property_elem.val(), policy_value.code);
}
}
@@ -471,9 +471,9 @@ function test_sync_realm_settings() {
{
/* Test message content edit limit minutes sync */
const property_elem = $("#id_realm_message_content_edit_limit_minutes");
property_elem.length = 1;
property_elem.attr("id", "id_realm_message_content_edit_limit_minutes");
const $property_elem = $("#id_realm_message_content_edit_limit_minutes");
$property_elem.length = 1;
$property_elem.attr("id", "id_realm_message_content_edit_limit_minutes");
page_params.realm_create_public_stream_policy = 1;
page_params.realm_message_content_edit_limit_seconds = 120;
@@ -484,9 +484,9 @@ function test_sync_realm_settings() {
{
/* Test message content edit limit dropdown value sync */
const property_elem = $("#id_realm_msg_edit_limit_setting");
property_elem.length = 1;
property_elem.attr("id", "id_realm_msg_edit_limit_setting");
const $property_elem = $("#id_realm_msg_edit_limit_setting");
$property_elem.length = 1;
$property_elem.attr("id", "id_realm_msg_edit_limit_setting");
page_params.realm_allow_message_editing = false;
page_params.realm_message_content_edit_limit_seconds = 120;
@@ -506,9 +506,9 @@ function test_sync_realm_settings() {
{
/* Test message content edit limit minutes sync */
const property_elem = $("#id_realm_message_content_edit_limit_minutes");
property_elem.length = 1;
property_elem.attr("id", "id_realm_message_content_edit_limit_minutes");
const $property_elem = $("#id_realm_message_content_edit_limit_minutes");
$property_elem.length = 1;
$property_elem.attr("id", "id_realm_message_content_edit_limit_minutes");
page_params.realm_create_public_stream_policy = 1;
page_params.realm_message_content_edit_limit_seconds = 120;
@@ -519,9 +519,9 @@ function test_sync_realm_settings() {
{
/* Test organization joining restrictions settings sync */
const property_elem = $("#id_realm_org_join_restrictions");
property_elem.length = 1;
property_elem.attr("id", "id_realm_org_join_restrictions");
const $property_elem = $("#id_realm_org_join_restrictions");
$property_elem.length = 1;
$property_elem.attr("id", "id_realm_org_join_restrictions");
page_params.realm_emails_restricted_to_domains = true;
page_params.realm_disallow_disposable_email_addresses = false;
@@ -541,10 +541,10 @@ function test_sync_realm_settings() {
}
function test_parse_time_limit() {
const elem = $("#id_realm_message_content_edit_limit_minutes");
const $elem = $("#id_realm_message_content_edit_limit_minutes");
const test_function = (value, expected_value = value) => {
elem.val(value);
page_params.realm_message_content_edit_limit_seconds = settings_org.parse_time_limit(elem);
$elem.val(value);
page_params.realm_message_content_edit_limit_seconds = settings_org.parse_time_limit($elem);
assert.equal(
settings_org.get_realm_time_limits_in_minutes(
"realm_message_content_edit_limit_seconds",
@@ -587,40 +587,40 @@ function test_discard_changes_button(discard_changes) {
settings_config.common_message_policy_values.by_everyone.code;
page_params.realm_message_content_delete_limit_seconds = 120;
const allow_edit_history = $("#id_realm_allow_edit_history").prop("checked", false);
const edit_topic_policy = $("#id_realm_edit_topic_policy").val(
const $allow_edit_history = $("#id_realm_allow_edit_history").prop("checked", false);
const $edit_topic_policy = $("#id_realm_edit_topic_policy").val(
settings_config.common_message_policy_values.by_admins_only.code,
);
const msg_edit_limit_setting = $("#id_realm_msg_edit_limit_setting").val("custom_limit");
const message_content_edit_limit_minutes = $(
const $msg_edit_limit_setting = $("#id_realm_msg_edit_limit_setting").val("custom_limit");
const $message_content_edit_limit_minutes = $(
"#id_realm_message_content_edit_limit_minutes",
).val(130);
const msg_delete_limit_setting = $("#id_realm_msg_delete_limit_setting").val("custom_limit");
const message_content_delete_limit_minutes = $(
const $msg_delete_limit_setting = $("#id_realm_msg_delete_limit_setting").val("custom_limit");
const $message_content_delete_limit_minutes = $(
"#id_realm_message_content_delete_limit_minutes",
).val(130);
allow_edit_history.attr("id", "id_realm_allow_edit_history");
msg_edit_limit_setting.attr("id", "id_realm_msg_edit_limit_setting");
msg_delete_limit_setting.attr("id", "id_realm_msg_delete_limit_setting");
edit_topic_policy.attr("id", "id_realm_edit_topic_policy");
message_content_edit_limit_minutes.attr("id", "id_realm_message_content_edit_limit_minutes");
message_content_delete_limit_minutes.attr(
$allow_edit_history.attr("id", "id_realm_allow_edit_history");
$msg_edit_limit_setting.attr("id", "id_realm_msg_edit_limit_setting");
$msg_delete_limit_setting.attr("id", "id_realm_msg_delete_limit_setting");
$edit_topic_policy.attr("id", "id_realm_edit_topic_policy");
$message_content_edit_limit_minutes.attr("id", "id_realm_message_content_edit_limit_minutes");
$message_content_delete_limit_minutes.attr(
"id",
"id_realm_message_content_delete_limit_minutes",
);
const discard_button_parent = $(".org-subsection-parent");
discard_button_parent.find = () => [
allow_edit_history,
msg_edit_limit_setting,
msg_delete_limit_setting,
edit_topic_policy,
message_content_edit_limit_minutes,
message_content_delete_limit_minutes,
const $discard_button_parent = $(".org-subsection-parent");
$discard_button_parent.find = () => [
$allow_edit_history,
$msg_edit_limit_setting,
$msg_delete_limit_setting,
$edit_topic_policy,
$message_content_edit_limit_minutes,
$message_content_delete_limit_minutes,
];
$("#org-discard-msg-editing").closest = () => discard_button_parent;
$("#org-discard-msg-editing").closest = () => $discard_button_parent;
const stubbed_function = settings_org.change_save_button_state;
settings_org.__Rewire__("change_save_button_state", (save_button_controls, state) => {
@@ -629,15 +629,15 @@ function test_discard_changes_button(discard_changes) {
discard_changes(ev);
assert.equal(allow_edit_history.prop("checked"), true);
assert.equal($allow_edit_history.prop("checked"), true);
assert.equal(
edit_topic_policy.val(),
$edit_topic_policy.val(),
settings_config.common_message_policy_values.by_everyone.code,
);
assert.equal(msg_edit_limit_setting.val(), "upto_one_hour");
assert.equal(message_content_edit_limit_minutes.val(), "60");
assert.equal(msg_delete_limit_setting.val(), "upto_two_min");
assert.equal(message_content_delete_limit_minutes.val(), "2");
assert.equal($msg_edit_limit_setting.val(), "upto_one_hour");
assert.equal($message_content_edit_limit_minutes.val(), "60");
assert.equal($msg_delete_limit_setting.val(), "upto_two_min");
assert.equal($message_content_delete_limit_minutes.val(), "2");
settings_org.__Rewire__("change_save_button_state", stubbed_function);
}
@@ -683,8 +683,8 @@ test("set_up", ({override, override_rewire, mock_template}) => {
$("#enable_digest_emails_label").set_parent($.create("<stub digest setting checkbox>"));
$("#id_realm_digest_weekday").set_parent($.create("<stub digest weekday setting dropdown>"));
$("#allowed_domains_label").set_parent($.create("<stub-allowed-domain-label-parent>"));
const waiting_period_parent_elem = $.create("waiting-period-parent-stub");
$("#id_realm_waiting_period_threshold").set_parent(waiting_period_parent_elem);
const $waiting_period_parent_elem = $.create("waiting-period-parent-stub");
$("#id_realm_waiting_period_threshold").set_parent($waiting_period_parent_elem);
$("#id_realm_create_web_public_stream_policy").set_parent(
$.create("<stub-create-web-public-stream-policy-parent>"),
);
@@ -829,8 +829,8 @@ test("test get_sorted_options_list", () => {
test("misc", ({override_rewire}) => {
page_params.is_admin = false;
const stub_notification_disable_parent = $.create("<stub notification_disable parent");
stub_notification_disable_parent.set_find_results(
const $stub_notification_disable_parent = $.create("<stub notification_disable parent");
$stub_notification_disable_parent.set_find_results(
".dropdown_list_reset_button",
$.create("<disable link>"),
);
@@ -904,14 +904,14 @@ test("misc", ({override_rewire}) => {
"realm_signup_notifications_stream_id",
"realm_default_code_block_language",
];
const dropdown_list_parent = $.create("<list parent>");
dropdown_list_parent.set_find_results(
const $dropdown_list_parent = $.create("<list parent>");
$dropdown_list_parent.set_find_results(
".dropdown_list_reset_button",
$.create("<disable button>"),
);
for (const name of widget_settings) {
const elem = $.create(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`);
elem.closest = () => dropdown_list_parent;
const $elem = $.create(`#${CSS.escape(name)}_widget #${CSS.escape(name)}_name`);
$elem.closest = () => $dropdown_list_parent;
}
// We do not define any settings we need in page_params yet, but we don't need to for this test.
@@ -923,32 +923,32 @@ test("misc", ({override_rewire}) => {
settings_org.init_dropdown_widgets();
let setting_name = "realm_notifications_stream_id";
let elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`);
elem.closest = () => stub_notification_disable_parent;
let $elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`);
$elem.closest = () => $stub_notification_disable_parent;
sub_store.__Rewire__("get", (stream_id) => {
assert.equal(stream_id, 42);
return {name: "some_stream"};
});
settings_org.notifications_stream_widget.render(42);
assert.equal(elem.text(), "#some_stream");
assert.ok(!elem.hasClass("text-warning"));
assert.equal($elem.text(), "#some_stream");
assert.ok(!$elem.hasClass("text-warning"));
settings_org.notifications_stream_widget.render(undefined);
assert.equal(elem.text(), "translated: Disabled");
assert.ok(elem.hasClass("text-warning"));
assert.equal($elem.text(), "translated: Disabled");
assert.ok($elem.hasClass("text-warning"));
setting_name = "realm_signup_notifications_stream_id";
elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`);
elem.closest = () => stub_notification_disable_parent;
$elem = $(`#${CSS.escape(setting_name)}_widget #${CSS.escape(setting_name)}_name`);
$elem.closest = () => $stub_notification_disable_parent;
sub_store.__Rewire__("get", (stream_id) => {
assert.equal(stream_id, 75);
return {name: "some_stream"};
});
settings_org.signup_notifications_stream_widget.render(75);
assert.equal(elem.text(), "#some_stream");
assert.ok(!elem.hasClass("text-warning"));
assert.equal($elem.text(), "#some_stream");
assert.ok(!$elem.hasClass("text-warning"));
settings_org.signup_notifications_stream_widget.render(undefined);
assert.equal(elem.text(), "translated: Disabled");
assert.ok(elem.hasClass("text-warning"));
assert.equal($elem.text(), "translated: Disabled");
assert.ok($elem.hasClass("text-warning"));
});

View File

@@ -43,19 +43,19 @@ function test_populate(opts, template_data) {
const fields_data = opts.fields_data;
page_params.is_admin = opts.is_admin;
const table = $("#admin_profile_fields_table");
const rows = $.create("rows");
const form = $.create("forms");
table.set_find_results("tr.profile-field-row", rows);
table.set_find_results("tr.profile-field-form", form);
const $table = $("#admin_profile_fields_table");
const $rows = $.create("rows");
const $form = $.create("forms");
$table.set_find_results("tr.profile-field-row", $rows);
$table.set_find_results("tr.profile-field-form", $form);
table[0] = "stub";
$table[0] = "stub";
rows.remove = () => {};
form.remove = () => {};
$rows.remove = () => {};
$form.remove = () => {};
let num_appends = 0;
table.append = () => {
$table.append = () => {
num_appends += 1;
};

View File

@@ -34,9 +34,9 @@ const settings_data = zrequire("settings_data");
const settings_user_groups = zrequire("settings_user_groups");
const user_pill = zrequire("user_pill");
function reset_test_setup(pill_container_stub) {
function reset_test_setup($pill_container_stub) {
function input_pill_stub(opts) {
assert.equal(opts.container, pill_container_stub);
assert.equal(opts.$container, $pill_container_stub);
create_item_handler = opts.create_item_from_text;
assert.ok(create_item_handler);
return pills;
@@ -119,18 +119,18 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
people.get_visible_email = () => bob.email;
let templates_render_called = false;
const fake_rendered_temp = $.create("fake_admin_user_group_list_template_rendered");
const $fake_rendered_temp = $.create("fake_admin_user_group_list_template_rendered");
mock_template("settings/admin_user_group_list.hbs", false, (args) => {
assert.equal(args.user_group.id, 1);
assert.equal(args.user_group.name, "Mobile");
assert.equal(args.user_group.description, "All mobile people");
templates_render_called = true;
return fake_rendered_temp;
return $fake_rendered_temp;
});
let user_groups_list_append_called = false;
$("#user-groups").append = (rendered_temp) => {
assert.equal(rendered_temp, fake_rendered_temp);
assert.equal(rendered_temp, $fake_rendered_temp);
user_groups_list_append_called = true;
};
@@ -158,7 +158,7 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
const all_pills = new Map();
const pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
const $pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
pills.appendValidatedData = (item) => {
const id = item.user_id;
assert.ok(!all_pills.has(id));
@@ -171,11 +171,11 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
text_cleared = true;
};
const input_field_stub = $.create("fake-input-field");
pill_container_stub.children = () => input_field_stub;
const $input_field_stub = $.create("fake-input-field");
$pill_container_stub.children = () => $input_field_stub;
let input_typeahead_called = false;
input_field_stub.typeahead = (config) => {
$input_field_stub.typeahead = (config) => {
assert.equal(config.items, 5);
assert.ok(config.fixed);
assert.ok(config.dropup);
@@ -187,9 +187,9 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
assert.equal(typeof config.updater, "function");
(function test_highlighter() {
const fake_person = $.create("fake-person");
typeahead_helper.render_person = () => fake_person;
assert.equal(config.highlighter(), fake_person);
const $fake_person = $.create("fake-person");
typeahead_helper.render_person = () => $fake_person;
assert.equal(config.highlighter(), $fake_person);
})();
const fake_context = {
@@ -236,7 +236,7 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
})();
(function test_updater() {
input_field_stub.text("@ali");
$input_field_stub.text("@ali");
user_groups.get_user_group_from_id = () => realm_user_group;
let saved_fade_out_called = false;
@@ -334,7 +334,7 @@ test_ui("populate_user_groups", ({override_rewire, mock_template}) => {
handler();
};
reset_test_setup(pill_container_stub);
reset_test_setup($pill_container_stub);
settings_user_groups.set_up();
assert.ok(templates_render_called);
assert.ok(user_groups_list_append_called);
@@ -386,63 +386,63 @@ test_ui("with_external_user", ({override_rewire, mock_template}) => {
$.clear_all_elements();
let user_group_find_called = 0;
const user_group_stub = $(`div.user-group[id="${CSS.escape(1)}"]`);
const name_field_stub = $.create("fake-name-field");
const description_field_stub = $.create("fake-description-field");
const input_stub = $.create("fake-input");
user_group_stub.find = (elem) => {
const $user_group_stub = $(`div.user-group[id="${CSS.escape(1)}"]`);
const $name_field_stub = $.create("fake-name-field");
const $description_field_stub = $.create("fake-description-field");
const $input_stub = $.create("fake-input");
$user_group_stub.find = (elem) => {
if (elem === ".name") {
user_group_find_called += 1;
return name_field_stub;
return $name_field_stub;
}
if (elem === ".description") {
user_group_find_called += 1;
return description_field_stub;
return $description_field_stub;
}
throw new Error(`Unknown element ${elem}`);
};
const pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
const pill_stub = $.create("fake-pill");
const $pill_container_stub = $(`.pill-container[data-group-pills="${CSS.escape(1)}"]`);
const $pill_stub = $.create("fake-pill");
let pill_container_find_called = 0;
pill_container_stub.find = (elem) => {
$pill_container_stub.find = (elem) => {
if (elem === ".input") {
pill_container_find_called += 1;
return input_stub;
return $input_stub;
}
if (elem === ".pill") {
pill_container_find_called += 1;
return pill_stub;
return $pill_stub;
}
throw new Error(`Unknown element ${elem}`);
};
input_stub.css = (property, val) => {
$input_stub.css = (property, val) => {
assert.equal(property, "display");
assert.equal(val, "none");
};
// Test the 'off' handlers on the pill-container
const turned_off = {};
pill_container_stub.off = (event_name, sel = "whole") => {
$pill_container_stub.off = (event_name, sel = "whole") => {
turned_off[event_name + "/" + sel] = true;
};
const exit_button = $.create("fake-pill-exit");
pill_stub.set_find_results(".exit", exit_button);
const $exit_button = $.create("fake-pill-exit");
$pill_stub.set_find_results(".exit", $exit_button);
let exit_button_called = false;
exit_button.css = (property, value) => {
$exit_button.css = (property, value) => {
exit_button_called = true;
assert.equal(property, "opacity");
assert.equal(value, "0.5");
};
// We return noop because these are already tested, so we skip them
pill_container_stub.children = () => noop;
$pill_container_stub.children = () => noop;
$("#user-groups").append = () => noop;
reset_test_setup(pill_container_stub);
reset_test_setup($pill_container_stub);
settings_user_groups.set_up();
@@ -451,8 +451,8 @@ test_ui("with_external_user", ({override_rewire, mock_template}) => {
// Test different handlers with an external user
const delete_handler = $("#user-groups").get_on_handler("click", ".delete");
const fake_delete = $.create("fk-#user-groups.delete_btn");
fake_delete.set_parents_result(".user-group", $(".user-group"));
const $fake_delete = $.create("fk-#user-groups.delete_btn");
$fake_delete.set_parents_result(".user-group", $(".user-group"));
set_parents_result_called += 1;
$(".user-group").attr("id", "1");
set_attributes_called += 1;
@@ -470,11 +470,11 @@ test_ui("with_external_user", ({override_rewire, mock_template}) => {
const event = {
stopPropagation: noop,
};
const pill_mouseenter_handler = pill_stub.get_on_handler("mouseenter");
const pill_click_handler = pill_container_stub.get_on_handler("click");
const pill_mouseenter_handler = $pill_stub.get_on_handler("mouseenter");
const pill_click_handler = $pill_container_stub.get_on_handler("click");
pill_mouseenter_handler(event);
pill_click_handler(event);
assert.equal(delete_handler.call(fake_delete), undefined);
assert.equal(delete_handler.call($fake_delete), undefined);
assert.equal(name_update_handler(), undefined);
assert.equal(des_update_handler(), undefined);
assert.equal(member_change_handler(), undefined);
@@ -524,7 +524,7 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
stopPropagation: noop,
preventDefault: noop,
};
const fake_this = $.create("fake-form.admin-user-group-form");
const $fake_this = $.create("fake-form.admin-user-group-form");
const fake_object_array = [
{
name: "fake-name",
@@ -535,7 +535,7 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
value: "fake-value",
},
];
fake_this.serializeArray = () => fake_object_array;
$fake_this.serializeArray = () => fake_object_array;
channel.post = (opts) => {
const data = {
members: "[null]",
@@ -577,13 +577,13 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
})();
};
handler.call(fake_this, event);
handler.call($fake_this, event);
})();
(function test_user_groups_delete_click_triggered() {
const handler = $("#user-groups").get_on_handler("click", ".delete");
const fake_this = $.create("fake-#user-groups.delete_btn");
fake_this.set_parents_result(".user-group", $(".user-group"));
const $fake_this = $.create("fake-#user-groups.delete_btn");
$fake_this.set_parents_result(".user-group", $(".user-group"));
$(".user-group").attr("id", "1");
channel.del = (opts) => {
@@ -593,16 +593,16 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
assert.equal(opts.url, "/json/user_groups/1");
assert.deepEqual(opts.data, data);
fake_this.text($t({defaultMessage: "fake-text"}));
$fake_this.text($t({defaultMessage: "fake-text"}));
opts.error();
assert.equal(fake_this.text(), "translated: Failed!");
assert.equal($fake_this.text(), "translated: Failed!");
};
confirm_dialog.launch = (conf) => {
conf.on_click();
};
handler.call(fake_this);
handler.call($fake_this);
})();
(function test_user_groups_keypress_enter_triggered() {
@@ -625,9 +625,10 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
api_endpoint_called = true;
};
channel.patch = noop;
const fake_this = $.create("fake-#user-groups_do_not_blur");
const $fake_this = $.create("fake-#user-groups_do_not_blur");
const event = {
relatedTarget: fake_this,
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
// Any of the blur_exceptions trigger blur event.
@@ -645,24 +646,24 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
continue;
}
api_endpoint_called = false;
fake_this.closest = (class_name) => {
$fake_this.closest = (class_name) => {
if (class_name === blur_exception || class_name === user_group_selector) {
return [1];
}
return [];
};
handler.call(fake_this, event);
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
}
api_endpoint_called = false;
fake_this.closest = (class_name) => {
$fake_this.closest = (class_name) => {
if (class_name === ".typeahead") {
return [1];
}
return [];
};
handler.call(fake_this, event);
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
// Cancel button triggers blur event.
@@ -671,7 +672,7 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
settings_user_groups_reload_called = true;
});
api_endpoint_called = false;
fake_this.closest = (class_name) => {
$fake_this.closest = (class_name) => {
if (
class_name === ".save-status.btn-danger" ||
class_name === user_group_selector
@@ -680,7 +681,7 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
}
return [];
};
handler.call(fake_this, event);
handler.call($fake_this, event);
assert.ok(!api_endpoint_called);
assert.ok(settings_user_groups_reload_called);
}
@@ -689,10 +690,10 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
(function test_update_cancel_button() {
const handler_name = $(user_group_selector).get_on_handler("input", ".name");
const handler_desc = $(user_group_selector).get_on_handler("input", ".description");
const sib_des = $(description_selector);
const sib_name = $(name_selector);
sib_name.text($t({defaultMessage: "mobile"}));
sib_des.text($t({defaultMessage: "All mobile members"}));
const $sib_des = $(description_selector);
const $sib_name = $(name_selector);
$sib_name.text($t({defaultMessage: "mobile"}));
$sib_des.text($t({defaultMessage: "All mobile members"}));
const group_data = {
name: "translated: mobile",
@@ -712,8 +713,8 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
};
// Cancel button removed if user group if user group has no changes.
const fake_this = $.create("fake-#update_cancel_button");
handler_name.call(fake_this);
const $fake_this = $.create("fake-#update_cancel_button");
handler_name.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
@@ -721,14 +722,14 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
$(user_group_selector + " .user-group-status").show();
cancel_fade_out_called = false;
instructions_fade_out_called = false;
handler_name.call(fake_this);
handler_name.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
// Check for handler_desc to achieve 100% coverage.
cancel_fade_out_called = false;
instructions_fade_out_called = false;
handler_desc.call(fake_this);
handler_desc.call($fake_this);
assert.ok(cancel_fade_out_called);
assert.ok(instructions_fade_out_called);
})();
@@ -736,10 +737,10 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
(function test_user_groups_save_group_changes_triggered() {
const handler_name = $(user_group_selector).get_on_handler("blur", ".name");
const handler_desc = $(user_group_selector).get_on_handler("blur", ".description");
const sib_des = $(description_selector);
const sib_name = $(name_selector);
sib_name.text($t({defaultMessage: "mobile"}));
sib_des.text($t({defaultMessage: "All mobile members"}));
const $sib_des = $(description_selector);
const $sib_name = $(name_selector);
$sib_name.text($t({defaultMessage: "mobile"}));
$sib_des.text($t({defaultMessage: "All mobile members"}));
const group_data = {members: new Set([2, 31])};
user_groups.get_user_group_from_id = () => group_data;
@@ -782,46 +783,47 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
assert.ok(saved_fade_to_called);
})();
(function test_post_error() {
const user_group_error = $(user_group_selector + " .user-group-status");
user_group_error.show();
const $user_group_error = $(user_group_selector + " .user-group-status");
$user_group_error.show();
ui_report.error = (error_msg, error_obj, ele) => {
const xhr = {
responseText: '{"msg":"fake-msg"}',
};
assert.equal(error_msg, "translated HTML: Failed");
assert.deepEqual(error_obj, xhr);
assert.equal(ele, user_group_error);
assert.equal(ele, $user_group_error);
};
const xhr = {
responseText: '{"msg":"fake-msg", "attrib":"val"}',
};
opts.error(xhr);
assert.ok(user_group_error.visible());
assert.ok($user_group_error.visible());
})();
};
const fake_this = $.create("fake-#user-groups_blur_name");
fake_this.closest = () => [];
fake_this.set_parents_result(user_group_selector, $(user_group_selector));
const $fake_this = $.create("fake-#user-groups_blur_name");
$fake_this.closest = () => [];
$fake_this.set_parents_result(user_group_selector, $(user_group_selector));
const event = {
relatedTarget: fake_this,
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
api_endpoint_called = false;
handler_name.call(fake_this, event);
handler_name.call($fake_this, event);
assert.ok(api_endpoint_called);
// Check API endpoint isn't called if name and desc haven't changed.
group_data.name = "translated: mobile";
group_data.description = "translated: All mobile members";
api_endpoint_called = false;
handler_name.call(fake_this, event);
handler_name.call($fake_this, event);
assert.ok(!api_endpoint_called);
// Check for handler_desc to achieve 100% coverage.
api_endpoint_called = false;
handler_desc.call(fake_this, event);
handler_desc.call($fake_this, event);
assert.ok(!api_endpoint_called);
})();
@@ -869,15 +871,16 @@ test_ui("on_events", ({override_rewire, mock_template}) => {
})();
};
const fake_this = $.create("fake-#user-groups_blur_input");
fake_this.set_parents_result(user_group_selector, $(user_group_selector));
fake_this.closest = () => [];
const $fake_this = $.create("fake-#user-groups_blur_input");
$fake_this.set_parents_result(user_group_selector, $(user_group_selector));
$fake_this.closest = () => [];
const event = {
relatedTarget: fake_this,
// FIXME: event.relatedTarget should not be a jQuery object
relatedTarget: $fake_this,
};
api_endpoint_called = false;
handler.call(fake_this, event);
handler.call($fake_this, event);
assert.ok(api_endpoint_called);
})();
});

View File

@@ -11,30 +11,30 @@ const spoilers = zrequire("spoilers");
// This function is taken from rendered_markdown.js and slightly modified.
const $array = (array) => {
const each = (func) => {
for (const [index, elem] of array.entries()) {
func.call(this, index, elem);
for (const [index, $elem] of array.entries()) {
func.call(this, index, $elem);
}
};
return {each};
};
const get_spoiler_elem = (title) => {
const block = $.create(`block-${title}`);
const header = $.create(`header-${title}`);
const content = $.create(`content-${title}`);
content.remove = () => {};
header.text(title);
block.set_find_results(".spoiler-header", header);
block.set_find_results(".spoiler-content", content);
return block;
const $block = $.create(`block-${title}`);
const $header = $.create(`header-${title}`);
const $content = $.create(`content-${title}`);
$content.remove = () => {};
$header.text(title);
$block.set_find_results(".spoiler-header", $header);
$block.set_find_results(".spoiler-content", $content);
return $block;
};
run_test("hide spoilers in notifications", () => {
const root = $.create("root element");
const spoiler_1 = get_spoiler_elem("this is the title");
const spoiler_2 = get_spoiler_elem("");
root.set_find_results(".spoiler-block", $array([spoiler_1, spoiler_2]));
spoilers.hide_spoilers_in_notification(root);
assert.equal(spoiler_1.find(".spoiler-header").text(), "this is the title (…)");
assert.equal(spoiler_2.find(".spoiler-header").text(), "(…)");
const $root = $.create("root element");
const $spoiler_1 = get_spoiler_elem("this is the title");
const $spoiler_2 = get_spoiler_elem("");
$root.set_find_results(".spoiler-block", $array([$spoiler_1, $spoiler_2]));
spoilers.hide_spoilers_in_notification($root);
assert.equal($spoiler_1.find(".spoiler-header").text(), "this is the title (…)");
assert.equal($spoiler_2.find(".spoiler-header").text(), "(…)");
});

View File

@@ -136,32 +136,32 @@ test("update_property", ({override}) => {
// Test desktop notifications
stream_events.update_property(stream_id, "desktop_notifications", true);
assert.equal(sub.desktop_notifications, true);
let checkbox = checkbox_for("desktop_notifications");
assert.equal(checkbox.prop("checked"), true);
let $checkbox = checkbox_for("desktop_notifications");
assert.equal($checkbox.prop("checked"), true);
// Tests audible notifications
stream_events.update_property(stream_id, "audible_notifications", true);
assert.equal(sub.audible_notifications, true);
checkbox = checkbox_for("audible_notifications");
assert.equal(checkbox.prop("checked"), true);
$checkbox = checkbox_for("audible_notifications");
assert.equal($checkbox.prop("checked"), true);
// Tests push notifications
stream_events.update_property(stream_id, "push_notifications", true);
assert.equal(sub.push_notifications, true);
checkbox = checkbox_for("push_notifications");
assert.equal(checkbox.prop("checked"), true);
$checkbox = checkbox_for("push_notifications");
assert.equal($checkbox.prop("checked"), true);
// Tests email notifications
stream_events.update_property(stream_id, "email_notifications", true);
assert.equal(sub.email_notifications, true);
checkbox = checkbox_for("email_notifications");
assert.equal(checkbox.prop("checked"), true);
$checkbox = checkbox_for("email_notifications");
assert.equal($checkbox.prop("checked"), true);
// Tests wildcard_mentions_notify notifications
stream_events.update_property(stream_id, "wildcard_mentions_notify", true);
assert.equal(sub.wildcard_mentions_notify, true);
checkbox = checkbox_for("wildcard_mentions_notify");
assert.equal(checkbox.prop("checked"), true);
$checkbox = checkbox_for("wildcard_mentions_notify");
assert.equal($checkbox.prop("checked"), true);
// Test name change
{
@@ -195,8 +195,8 @@ test("update_property", ({override}) => {
{
override(stream_list, "refresh_pinned_or_unpinned_stream", noop);
stream_events.update_property(stream_id, "pin_to_top", true);
checkbox = checkbox_for("pin_to_top");
assert.equal(checkbox.prop("checked"), true);
$checkbox = checkbox_for("pin_to_top");
assert.equal($checkbox.prop("checked"), true);
}
// Test stream privacy change event

View File

@@ -19,7 +19,7 @@ const topic_list = mock_esm("../../static/js/topic_list");
mock_esm("../../static/js/keydown_util", {
handle: noop,
});
mock_esm("../../static/js/ui", {get_scroll_element: (element) => element});
mock_esm("../../static/js/ui", {get_scroll_element: ($element) => $element});
const {Filter} = zrequire("../js/filter");
const stream_sort = zrequire("stream_sort");
@@ -47,41 +47,41 @@ const social = {
let num_unread_for_stream;
function create_devel_sidebar_row({mock_template}) {
const devel_count = $.create("devel-count");
const subscription_block = $.create("devel-block");
const $devel_count = $.create("devel-count");
const $subscription_block = $.create("devel-block");
const sidebar_row = $("<devel sidebar row>");
const $sidebar_row = $("<devel-sidebar-row-stub>");
sidebar_row.set_find_results(".subscription_block", subscription_block);
subscription_block.set_find_results(".unread_count", devel_count);
$sidebar_row.set_find_results(".subscription_block", $subscription_block);
$subscription_block.set_find_results(".unread_count", $devel_count);
mock_template("stream_sidebar_row.hbs", false, (data) => {
assert.equal(data.uri, "#narrow/stream/100-devel");
return "<devel sidebar row>";
return "<devel-sidebar-row-stub>";
});
num_unread_for_stream = 42;
stream_list.create_sidebar_row(devel);
assert.equal(devel_count.text(), "42");
assert.equal($devel_count.text(), "42");
}
function create_social_sidebar_row({mock_template}) {
const social_count = $.create("social-count");
const subscription_block = $.create("social-block");
const $social_count = $.create("social-count");
const $subscription_block = $.create("social-block");
const sidebar_row = $("<social sidebar row>");
const $sidebar_row = $("<social-sidebar-row-stub>");
sidebar_row.set_find_results(".subscription_block", subscription_block);
subscription_block.set_find_results(".unread_count", social_count);
$sidebar_row.set_find_results(".subscription_block", $subscription_block);
$subscription_block.set_find_results(".unread_count", $social_count);
mock_template("stream_sidebar_row.hbs", false, (data) => {
assert.equal(data.uri, "#narrow/stream/200-social");
return "<social sidebar row>";
return "<social-sidebar-row-stub>";
});
num_unread_for_stream = 99;
stream_list.create_sidebar_row(social);
assert.equal(social_count.text(), "99");
assert.equal($social_count.text(), "99");
}
function test_ui(label, f) {
@@ -105,8 +105,8 @@ test_ui("create_sidebar_row", ({override_rewire, mock_template}) => {
create_social_sidebar_row({mock_template});
const split = '<hr class="stream-split">';
const devel_sidebar = $("<devel sidebar row>");
const social_sidebar = $("<social sidebar row>");
const $devel_sidebar = $("<devel-sidebar-row-stub>");
const $social_sidebar = $("<social-sidebar-row-stub>");
let appended_elems;
$("#stream_filters").append = (elems) => {
@@ -123,20 +123,20 @@ test_ui("create_sidebar_row", ({override_rewire, mock_template}) => {
assert.ok(topic_list_cleared);
const expected_elems = [
devel_sidebar, // pinned
$devel_sidebar, // pinned
split, // separator
social_sidebar, // not pinned
$social_sidebar, // not pinned
];
assert.deepEqual(appended_elems, expected_elems);
const social_li = $("<social sidebar row>");
const $social_li = $("<social-sidebar-row-stub>");
const stream_id = social.stream_id;
social_li.length = 0;
$social_li.length = 0;
const privacy_elem = $.create("privacy-stub");
social_li.set_find_results(".stream-privacy", privacy_elem);
const $privacy_elem = $.create("privacy-stub");
$social_li.set_find_results(".stream-privacy", $privacy_elem);
social.invite_only = true;
social.color = "#222222";
@@ -147,25 +147,25 @@ test_ui("create_sidebar_row", ({override_rewire, mock_template}) => {
return "<div>privacy-html";
});
stream_list.redraw_stream_privacy(social);
assert.equal(privacy_elem.html(), "<div>privacy-html");
assert.equal($privacy_elem.html(), "<div>privacy-html");
stream_list.set_in_home_view(stream_id, false);
assert.ok(social_li.hasClass("out_of_home_view"));
assert.ok($social_li.hasClass("out_of_home_view"));
stream_list.set_in_home_view(stream_id, true);
assert.ok(!social_li.hasClass("out_of_home_view"));
assert.ok(!$social_li.hasClass("out_of_home_view"));
const row = stream_list.stream_sidebar.get_row(stream_id);
override_rewire(stream_data, "is_active", () => true);
row.update_whether_active();
assert.ok(!social_li.hasClass("inactive_stream"));
assert.ok(!$social_li.hasClass("inactive_stream"));
override_rewire(stream_data, "is_active", () => false);
row.update_whether_active();
assert.ok(social_li.hasClass("inactive_stream"));
assert.ok($social_li.hasClass("inactive_stream"));
let removed;
social_li.remove = () => {
$social_li.remove = () => {
removed = true;
};
@@ -183,33 +183,33 @@ test_ui("pinned_streams_never_inactive", ({override_rewire, mock_template}) => {
create_social_sidebar_row({mock_template});
// non-pinned streams can be made inactive
const social_sidebar = $("<social sidebar row>");
const $social_sidebar = $("<social-sidebar-row-stub>");
let stream_id = social.stream_id;
let row = stream_list.stream_sidebar.get_row(stream_id);
override_rewire(stream_data, "is_active", () => false);
stream_list.build_stream_list();
assert.ok(social_sidebar.hasClass("inactive_stream"));
assert.ok($social_sidebar.hasClass("inactive_stream"));
override_rewire(stream_data, "is_active", () => true);
row.update_whether_active();
assert.ok(!social_sidebar.hasClass("inactive_stream"));
assert.ok(!$social_sidebar.hasClass("inactive_stream"));
override_rewire(stream_data, "is_active", () => false);
row.update_whether_active();
assert.ok(social_sidebar.hasClass("inactive_stream"));
assert.ok($social_sidebar.hasClass("inactive_stream"));
// pinned streams can never be made inactive
const devel_sidebar = $("<devel sidebar row>");
const $devel_sidebar = $("<devel-sidebar-row-stub>");
stream_id = devel.stream_id;
row = stream_list.stream_sidebar.get_row(stream_id);
override_rewire(stream_data, "is_active", () => false);
stream_list.build_stream_list();
assert.ok(!devel_sidebar.hasClass("inactive_stream"));
assert.ok(!$devel_sidebar.hasClass("inactive_stream"));
row.update_whether_active();
assert.ok(!devel_sidebar.hasClass("inactive_stream"));
assert.ok(!$devel_sidebar.hasClass("inactive_stream"));
});
function add_row(sub) {
@@ -217,12 +217,12 @@ function add_row(sub) {
const row = {
update_whether_active() {},
get_li() {
const html = "<" + sub.name + " sidebar row html>";
const obj = $(html);
const html = "<" + sub.name + "-sidebar-row-stub>";
const $obj = $(html);
obj.length = 1; // bypass blueslip error
$obj.length = 1; // bypass blueslip error
return obj;
return $obj;
},
};
stream_list.stream_sidebar.set_row(sub.stream_id, row);
@@ -293,30 +293,30 @@ function elem($obj) {
}
test_ui("zoom_in_and_zoom_out", () => {
const label1 = $.create("label1 stub");
const label2 = $.create("label2 stub");
const $label1 = $.create("label1 stub");
const $label2 = $.create("label2 stub");
label1.show();
label2.show();
$label1.show();
$label2.show();
assert.ok(label1.visible());
assert.ok(label2.visible());
assert.ok($label1.visible());
assert.ok($label2.visible());
$.create(".stream-filters-label", {
children: [elem(label1), elem(label2)],
children: [elem($label1), elem($label2)],
});
const splitter = $.create("hr stub");
const $splitter = $.create("hr stub");
splitter.show();
assert.ok(splitter.visible());
$splitter.show();
assert.ok($splitter.visible());
$.create(".stream-split", {
children: [elem(splitter)],
children: [elem($splitter)],
});
const stream_li1 = $.create("stream1 stub");
const stream_li2 = $.create("stream2 stub");
const $stream_li1 = $.create("stream1 stub");
const $stream_li2 = $.create("stream2 stub");
function make_attr(arg) {
return (sel) => {
@@ -325,12 +325,12 @@ test_ui("zoom_in_and_zoom_out", () => {
};
}
stream_li1.attr = make_attr("42");
stream_li1.hide();
stream_li2.attr = make_attr("99");
$stream_li1.attr = make_attr("42");
$stream_li1.hide();
$stream_li2.attr = make_attr("99");
$.create("#stream_filters li.narrow-filter", {
children: [elem(stream_li1), elem(stream_li2)],
children: [elem($stream_li1), elem($stream_li2)],
});
$("#stream-filters-container")[0] = {
@@ -340,26 +340,26 @@ test_ui("zoom_in_and_zoom_out", () => {
stream_list.zoom_in_topics({stream_id: 42});
assert.ok(!label1.visible());
assert.ok(!label2.visible());
assert.ok(!splitter.visible());
assert.ok(stream_li1.visible());
assert.ok(!stream_li2.visible());
assert.ok(!$label1.visible());
assert.ok(!$label2.visible());
assert.ok(!$splitter.visible());
assert.ok($stream_li1.visible());
assert.ok(!$stream_li2.visible());
assert.ok($("#streams_list").hasClass("zoom-in"));
$("#stream_filters li.narrow-filter").show = () => {
stream_li1.show();
stream_li2.show();
$stream_li1.show();
$stream_li2.show();
};
stream_li1.length = 1;
stream_list.zoom_out_topics({stream_li: stream_li1});
$stream_li1.length = 1;
stream_list.zoom_out_topics({$stream_li: $stream_li1});
assert.ok(label1.visible());
assert.ok(label2.visible());
assert.ok(splitter.visible());
assert.ok(stream_li1.visible());
assert.ok(stream_li2.visible());
assert.ok($label1.visible());
assert.ok($label2.visible());
assert.ok($splitter.visible());
assert.ok($stream_li1.visible());
assert.ok($stream_li2.visible());
assert.ok($("#streams_list").hasClass("zoom-out"));
});
@@ -372,7 +372,7 @@ test_ui("narrowing", ({override_rewire}) => {
topic_list.get_stream_li = noop;
override_rewire(scroll_util, "scroll_element_into_container", noop);
assert.ok(!$("<devel sidebar row html>").hasClass("active-filter"));
assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter"));
stream_list.set_event_handlers();
@@ -380,7 +380,7 @@ test_ui("narrowing", ({override_rewire}) => {
filter = new Filter([{operator: "stream", operand: "devel"}]);
stream_list.handle_narrow_activated(filter);
assert.ok($("<devel sidebar row html>").hasClass("active-filter"));
assert.ok($("<devel-sidebar-row-stub>").hasClass("active-filter"));
filter = new Filter([
{operator: "stream", operand: "cars"},
@@ -388,12 +388,12 @@ test_ui("narrowing", ({override_rewire}) => {
]);
stream_list.handle_narrow_activated(filter);
assert.ok(!$("ul.filters li").hasClass("active-filter"));
assert.ok(!$("<cars sidebar row html>").hasClass("active-filter")); // false because of topic
assert.ok(!$("<cars-sidebar-row-stub>").hasClass("active-filter")); // false because of topic
filter = new Filter([{operator: "stream", operand: "cars"}]);
stream_list.handle_narrow_activated(filter);
assert.ok(!$("ul.filters li").hasClass("active-filter"));
assert.ok($("<cars sidebar row html>").hasClass("active-filter"));
assert.ok($("<cars-sidebar-row-stub>").hasClass("active-filter"));
let removed_classes;
$("ul#stream_filters li").removeClass = (classes) => {
@@ -450,14 +450,14 @@ test_ui("sort_streams", ({override_rewire}) => {
const split = '<hr class="stream-split">';
const expected_elems = [
$("<devel sidebar row html>"),
$("<Rome sidebar row html>"),
$("<test sidebar row html>"),
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
$("<test-sidebar-row-stub>"),
split,
$("<announce sidebar row html>"),
$("<Denmark sidebar row html>"),
$("<announce-sidebar-row-stub>"),
$("<Denmark-sidebar-row-stub>"),
split,
$("<cars sidebar row html>"),
$("<cars-sidebar-row-stub>"),
];
assert.deepEqual(appended_elems, expected_elems);
@@ -529,11 +529,11 @@ test_ui("separators_only_pinned_and_dormant", ({override_rewire}) => {
const split = '<hr class="stream-split">';
const expected_elems = [
// pinned
$("<devel sidebar row html>"),
$("<Rome sidebar row html>"),
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
split,
// dormant
$("<Denmark sidebar row html>"),
$("<Denmark-sidebar-row-stub>"),
];
assert.deepEqual(appended_elems, expected_elems);
@@ -573,8 +573,8 @@ test_ui("separators_only_pinned", () => {
const expected_elems = [
// pinned
$("<devel sidebar row html>"),
$("<Rome sidebar row html>"),
$("<devel-sidebar-row-stub>"),
$("<Rome-sidebar-row-stub>"),
// no separator at the end as no stream follows
];
@@ -591,8 +591,8 @@ test_ui("rename_stream", ({override_rewire, mock_template}) => {
stream_data.rename_sub(sub, new_name);
const li_stub = $.create("li stub");
li_stub.length = 0;
const $li_stub = $.create("li stub");
$li_stub.length = 0;
mock_template("stream_sidebar_row.hbs", false, (payload) => {
assert.deepEqual(payload, {
@@ -606,12 +606,12 @@ test_ui("rename_stream", ({override_rewire, mock_template}) => {
pin_to_top: true,
dark_background: payload.dark_background,
});
return {to_$: () => li_stub};
return {to_$: () => $li_stub};
});
let count_updated;
override_rewire(stream_list, "update_count_in_dom", (li) => {
assert.equal(li, li_stub);
override_rewire(stream_list, "update_count_in_dom", ($li) => {
assert.equal($li, $li_stub);
count_updated = true;
});
@@ -638,17 +638,17 @@ test_ui("refresh_pin", ({override_rewire, mock_template}) => {
pin_to_top: true,
};
const li_stub = $.create("li stub");
li_stub.length = 0;
const $li_stub = $.create("li stub");
$li_stub.length = 0;
mock_template("stream_sidebar_row.hbs", false, () => ({to_$: () => li_stub}));
mock_template("stream_sidebar_row.hbs", false, () => ({to_$: () => $li_stub}));
override_rewire(stream_list, "update_count_in_dom", noop);
$("#stream_filters").append = noop;
let scrolled;
override_rewire(stream_list, "scroll_stream_into_view", (li) => {
assert.equal(li, li_stub);
override_rewire(stream_list, "scroll_stream_into_view", ($li) => {
assert.equal($li, $li_stub);
scrolled = true;
});

View File

@@ -64,32 +64,32 @@ function clear_search_input() {
run_test("basics", ({override_rewire}) => {
let cursor_helper;
const input = $(".stream-list-filter");
const section = $(".stream_search_section");
const $input = $(".stream-list-filter");
const $section = $(".stream_search_section");
expand_sidebar();
section.addClass("notdisplayed");
$section.addClass("notdisplayed");
cursor_helper = make_cursor_helper();
function verify_expanded() {
assert.ok(!section.hasClass("notdisplayed"));
assert.ok(!$section.hasClass("notdisplayed"));
simulate_search_expanded();
}
function verify_focused() {
assert.ok(stream_list.searching());
assert.ok(input.is_focused());
assert.ok($input.is_focused());
}
function verify_blurred() {
assert.ok(stream_list.searching());
assert.ok(input.is_focused());
assert.ok($input.is_focused());
}
function verify_collapsed() {
assert.ok(section.hasClass("notdisplayed"));
assert.ok(!input.is_focused());
assert.ok($section.hasClass("notdisplayed"));
assert.ok(!$input.is_focused());
assert.ok(!stream_list.searching());
simulate_search_collapsed();
}
@@ -126,7 +126,7 @@ run_test("basics", ({override_rewire}) => {
(function add_some_text_and_collapse() {
cursor_helper = make_cursor_helper();
input.val("foo");
$input.val("foo");
verify_list_updated(() => {
toggle_filter();
});
@@ -149,7 +149,7 @@ run_test("basics", ({override_rewire}) => {
stream_list.initiate_search();
// Clear a non-empty search.
input.val("foo");
$input.val("foo");
verify_list_updated(() => {
clear_search_input();
});
@@ -160,7 +160,7 @@ run_test("basics", ({override_rewire}) => {
stream_list.initiate_search();
// Escape a non-empty search.
input.val("foo");
$input.val("foo");
stream_list.escape_search();
verify_blurred();
@@ -169,7 +169,7 @@ run_test("basics", ({override_rewire}) => {
stream_list.initiate_search();
// Escape an empty search.
input.val("");
$input.val("");
stream_list.escape_search();
verify_collapsed();
});

View File

@@ -9,8 +9,8 @@ const $ = require("../zjsunit/zjquery");
const denmark_stream_id = 101;
const ui = mock_esm("../../static/js/ui", {
get_content_element: (element) => element,
get_scroll_element: (element) => element,
get_content_element: ($element) => $element,
get_scroll_element: ($element) => $element,
});
mock_esm("../../static/js/hash_util", {
@@ -103,16 +103,16 @@ run_test("redraw_left_panel", ({mock_template}) => {
$.create("#manage_streams_container .stream-row", {children: sub_stubs});
let ui_called = false;
ui.reset_scrollbar = (elem) => {
ui.reset_scrollbar = ($elem) => {
ui_called = true;
assert.equal(elem, $("#subscription_overlay .streams-list"));
assert.equal($elem, $("#subscription_overlay .streams-list"));
};
// Filtering has the side effect of setting the "active" class
// on our current stream, even if it doesn't match the filter.
const denmark_row = $(`.stream-row[data-stream-id='${CSS.escape(denmark_stream_id)}']`);
const $denmark_row = $(`.stream-row[data-stream-id='${CSS.escape(denmark_stream_id)}']`);
// sanity check it's not set to active
assert.ok(!denmark_row.hasClass("active"));
assert.ok(!$denmark_row.hasClass("active"));
function test_filter(params, expected_streams) {
const stream_ids = stream_settings_ui.redraw_left_panel(params);
@@ -127,7 +127,7 @@ run_test("redraw_left_panel", ({mock_template}) => {
assert.ok(ui_called);
// The denmark row is active, even though it's not displayed.
assert.ok(denmark_row.hasClass("active"));
assert.ok($denmark_row.hasClass("active"));
// Search with multiple keywords
test_filter({input: "Denmark, Pol", subscribed_only: false}, [denmark, poland]);

View File

@@ -19,14 +19,14 @@ run_test("scrub_realm", () => {
$.get_initialize_function()();
const click_handler = $("body").get_on_handler("click", ".scrub-realm-button");
const fake_this = $.create("fake-.scrub-realm-button");
fake_this.data = (name) => {
const $fake_this = $.create("fake-.scrub-realm-button");
$fake_this.data = (name) => {
assert.equal(name, "string-id");
return "zulip";
};
let submit_form_called = false;
fake_this.form = {
$fake_this.form = {
submit: () => {
submit_form_called = true;
},
@@ -36,7 +36,7 @@ run_test("scrub_realm", () => {
};
window.prompt = () => "zulip";
click_handler.call(fake_this, event);
click_handler.call($fake_this, event);
assert.ok(submit_form_called);
submit_form_called = false;
@@ -45,7 +45,7 @@ run_test("scrub_realm", () => {
window.alert = () => {
alert_called = true;
};
click_handler.call(fake_this, event);
click_handler.call($fake_this, event);
assert.ok(!submit_form_called);
assert.ok(alert_called);

View File

@@ -210,20 +210,20 @@ run_test("render_date_renders_time_html", () => {
const expected_html = $t({defaultMessage: "Today"});
const attrs = {};
const span_stub = $("<span />");
const $span_stub = $("<span>");
span_stub.attr = (name, val) => {
$span_stub.attr = (name, val) => {
attrs[name] = val;
return span_stub;
return $span_stub;
};
span_stub.append = (str) => {
span_stub.html(str);
return span_stub;
$span_stub.append = (str) => {
$span_stub.html(str);
return $span_stub;
};
const actual = timerender.render_date(message_time, undefined, today);
assert.equal(actual.html(), expected_html);
const $actual = timerender.render_date(message_time, undefined, today);
assert.equal($actual.html(), expected_html);
assert.equal(attrs["data-tippy-content"], "Friday, April 12, 2019");
assert.equal(attrs.class, "timerender0");
});
@@ -234,19 +234,19 @@ run_test("render_date_renders_time_above_html", () => {
const message_time = today;
const message_time_above = add(today, {days: -1});
const span_stub = $("<span />");
const $span_stub = $("<span>");
let appended_val;
span_stub.append = (...val) => {
$span_stub.append = (...val) => {
appended_val = val;
return span_stub;
return $span_stub;
};
const expected = [
'<i class="date-direction fa fa-caret-up"></i>',
$("<i>"),
$t({defaultMessage: "Yesterday"}),
'<hr class="date-line">',
'<i class="date-direction fa fa-caret-down"></i>',
$("<hr>"),
$("<i>"),
$t({defaultMessage: "Today"}),
];

View File

@@ -104,12 +104,12 @@ run_test("narrowing", ({override_rewire}) => {
});
run_test("update_count_in_dom", () => {
function make_elem(elem, count_selector) {
const count = $(count_selector);
elem.set_find_results(".unread_count", count);
count.set_parent(elem);
function make_elem($elem, count_selector) {
const $count = $(count_selector);
$elem.set_find_results(".unread_count", $count);
$count.set_parent($elem);
return elem;
return $elem;
}
const counts = {

View File

@@ -39,7 +39,7 @@ people.add_active_user(levin);
people.add_active_user(kitty);
run_test("render_notifications_for_narrow", ({override_rewire, mock_template}) => {
const typing_notifications = $("#typing_notifications");
const $typing_notifications = $("#typing_notifications");
const two_typing_users_ids = [anna.user_id, vronsky.user_id];
const three_typing_users_ids = [anna.user_id, vronsky.user_id, levin.user_id];
@@ -51,32 +51,32 @@ run_test("render_notifications_for_narrow", ({override_rewire, mock_template}) =
// should be rendered but not 'Several people are typing…'
override_rewire(typing_events, "get_users_typing_for_narrow", () => two_typing_users_ids);
typing_events.render_notifications_for_narrow();
assert.ok(typing_notifications.visible());
assert.ok(typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes("Several people are typing…"));
assert.ok($typing_notifications.visible());
assert.ok($typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok($typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes("Several people are typing…"));
// Having 3(=MAX_USERS_TO_DISPLAY_NAME) typists should also display only names
override_rewire(typing_events, "get_users_typing_for_narrow", () => three_typing_users_ids);
typing_events.render_notifications_for_narrow();
assert.ok(typing_notifications.visible());
assert.ok(typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes("Several people are typing…"));
assert.ok($typing_notifications.visible());
assert.ok($typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok($typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok($typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes("Several people are typing…"));
// Having 4(>MAX_USERS_TO_DISPLAY_NAME) typists should display "Several people are typing…"
override_rewire(typing_events, "get_users_typing_for_narrow", () => four_typing_users_ids);
typing_events.render_notifications_for_narrow();
assert.ok(typing_notifications.visible());
assert.ok(typing_notifications.html().includes("Several people are typing…"));
assert.ok(!typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!typing_notifications.html().includes(`${kitty.full_name} is typing…`));
assert.ok($typing_notifications.visible());
assert.ok($typing_notifications.html().includes("Several people are typing…"));
assert.ok(!$typing_notifications.html().includes(`${anna.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${vronsky.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${levin.full_name} is typing…`));
assert.ok(!$typing_notifications.html().includes(`${kitty.full_name} is typing…`));
// #typing_notifications should be hidden when there are no typists.
override_rewire(typing_events, "get_users_typing_for_narrow", () => []);
typing_events.render_notifications_for_narrow();
assert.ok(!typing_notifications.visible());
assert.ok(!$typing_notifications.visible());
});

View File

@@ -15,11 +15,7 @@ let uppy_stub;
function Uppy(options) {
return uppy_stub.call(this, options);
}
Uppy.Plugin = {
prototype: {
constructor: null,
},
};
Uppy.UIPlugin = class UIPlugin {};
mock_cjs("@uppy/core", Uppy);
mock_esm("../../static/js/csrf", {csrf_token: "csrf_token"});
@@ -36,14 +32,14 @@ function test(label, f) {
}
test("feature_check", ({override}) => {
const upload_button = $.create("upload-button-stub");
upload_button.addClass("notdisplayed");
upload.feature_check(upload_button);
assert.ok(upload_button.hasClass("notdisplayed"));
const $upload_button = $.create("upload-button-stub");
$upload_button.addClass("notdisplayed");
upload.feature_check($upload_button);
assert.ok($upload_button.hasClass("notdisplayed"));
override(window, "XMLHttpRequest", () => ({upload: true}));
upload.feature_check(upload_button);
assert.ok(!upload_button.hasClass("notdisplayed"));
upload.feature_check($upload_button);
assert.ok(!$upload_button.hasClass("notdisplayed"));
});
test("get_item", () => {

View File

@@ -194,7 +194,7 @@ test("filter_user_ids", ({override}) => {
});
test("click on user header to toggle display", ({override}) => {
const user_filter = $(".user-list-filter");
const $user_filter = $(".user-list-filter");
override(popovers, "hide_all", () => {});
override(popovers, "hide_all_except_sidebars", () => {});
@@ -205,11 +205,11 @@ test("click on user header to toggle display", ({override}) => {
assert.ok(!$("#user_search_section").hasClass("notdisplayed"));
user_filter.val("bla");
$user_filter.val("bla");
$("#userlist-header").trigger("click");
assert.ok($("#user_search_section").hasClass("notdisplayed"));
assert.equal(user_filter.val(), "");
assert.equal($user_filter.val(), "");
$(".user-list-filter").closest = (selector) => {
assert.equal(selector, ".app-main [class^='column-']");

View File

@@ -282,12 +282,18 @@ run_test("clean_user_content_links", () => {
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/file.png">upload</a> ' +
'<a href="http://localhost:NNNN">invalid</a> ' +
'<a href="javascript:alert(1)">unsafe</a> ' +
'<a href="/#fragment" target="_blank">fragment</a>',
'<a href="/#fragment" target="_blank">fragment</a>' +
'<div class="message_inline_image">' +
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.png" title="inline image">upload</a> ' +
"</div>",
),
'<a href="http://example.com" target="_blank" rel="noopener noreferrer" title="http://example.com/">good</a> ' +
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/file.png" target="_blank" rel="noopener noreferrer" title="translated: Download file.png">upload</a> ' +
"<a>invalid</a> " +
"<a>unsafe</a> " +
'<a href="/#fragment" title="http://zulip.zulipdev.com/#fragment">fragment</a>',
'<a href="/#fragment" title="http://zulip.zulipdev.com/#fragment">fragment</a>' +
'<div class="message_inline_image">' +
'<a href="http://zulip.zulipdev.com/user_uploads/w/ha/tever/inline.png" target="_blank" rel="noopener noreferrer" aria-label="inline image">upload</a> ' +
"</div>",
);
});

View File

@@ -35,16 +35,16 @@ const sample_events = [
];
let events;
let widget_elem;
let $widget_elem;
let is_event_handled;
let is_widget_activated;
const fake_poll_widget = {
activate(data) {
is_widget_activated = true;
widget_elem = data.elem;
assert.ok(widget_elem.hasClass("widget-content"));
widget_elem.handle_events = (e) => {
$widget_elem = data.$elem;
assert.ok($widget_elem.hasClass("widget-content"));
$widget_elem.handle_events = (e) => {
is_event_handled = true;
assert.notDeepStrictEqual(e, events);
events.shift();
@@ -65,7 +65,7 @@ const widgetize = zrequire("widgetize");
function test(label, f) {
run_test(label, ({override}) => {
events = [...sample_events];
widget_elem = undefined;
$widget_elem = undefined;
is_event_handled = false;
is_widget_activated = false;
widgetize.clear_for_testing();
@@ -76,10 +76,10 @@ function test(label, f) {
test("activate", ({override}) => {
// Both widgetize.activate and widgetize.handle_event are tested
// here to use the "caching" of widgets
const row = $.create("<stub message row>");
row.attr("id", "zhome2909");
const message_content = $.create("#zhome2909");
row.set_find_results(".message_content", message_content);
const $row = $.create("<stub message row>");
$row.attr("id", "zhome2909");
const $message_content = $.create("#zhome2909");
$row.set_find_results(".message_content", $message_content);
let narrow_active;
override(narrow_state, "active", () => narrow_active);
@@ -94,16 +94,16 @@ test("activate", ({override}) => {
assert.equal(data.msg_type, "widget");
assert.equal(data.data, "test_data");
},
row,
$row,
widget_type: "poll",
};
let is_widget_elem_inserted;
message_content.append = (elem) => {
$message_content.append = ($elem) => {
is_widget_elem_inserted = true;
assert.equal(elem, widget_elem);
assert.ok(elem.hasClass("widget-content"));
assert.equal($elem, $widget_elem);
assert.ok($elem.hasClass("widget-content"));
};
is_widget_elem_inserted = false;
@@ -111,19 +111,19 @@ test("activate", ({override}) => {
is_event_handled = false;
assert.ok(!widgetize.widget_contents.has(opts.message.id));
message_content.set_find_results(".widget-content", false);
$message_content.set_find_results(".widget-content", false);
widgetize.activate(opts);
assert.ok(is_widget_elem_inserted);
assert.ok(is_widget_activated);
assert.ok(is_event_handled);
assert.equal(widgetize.widget_contents.get(opts.message.id), widget_elem);
assert.equal(widgetize.widget_contents.get(opts.message.id), $widget_elem);
is_widget_elem_inserted = false;
is_widget_activated = false;
is_event_handled = false;
message_content.set_find_results(".widget-content", false);
$message_content.set_find_results(".widget-content", false);
widgetize.activate(opts);
assert.ok(is_widget_elem_inserted);
@@ -135,7 +135,7 @@ test("activate", ({override}) => {
is_widget_activated = false;
is_event_handled = false;
message_content.set_find_results(".widget-content", false);
$message_content.set_find_results(".widget-content", false);
widgetize.activate(opts);
assert.ok(!is_widget_elem_inserted);
@@ -171,7 +171,7 @@ test("activate", ({override}) => {
message_id: 2001,
sender_id: 102,
};
widget_elem.handle_events = (e) => {
$widget_elem.handle_events = (e) => {
is_event_handled = true;
assert.deepEqual(e, [post_activate_event]);
};
@@ -191,7 +191,7 @@ test("activate", ({override}) => {
});
override(message_lists.current, "get_row", (idx) => {
assert.equal(idx, 2001);
return row;
return $row;
});
widgetize.set_widgets_for_list();
});

View File

@@ -45,28 +45,28 @@ run_test("basics", () => {
// Next, look at how several functions correctly simulate setting
// and getting for you.
const widget = $("#my-widget");
const $widget = $("#my-widget");
widget.attr("data-employee-id", 42);
assert.equal(widget.attr("data-employee-id"), 42);
assert.equal(widget.data("employee-id"), 42);
$widget.attr("data-employee-id", 42);
assert.equal($widget.attr("data-employee-id"), 42);
assert.equal($widget.data("employee-id"), 42);
widget.data("department-id", 77);
assert.equal(widget.attr("data-department-id"), 77);
assert.equal(widget.data("department-id"), 77);
$widget.data("department-id", 77);
assert.equal($widget.attr("data-department-id"), 77);
assert.equal($widget.data("department-id"), 77);
widget.data("department-name", "hr");
assert.equal(widget.attr("data-department-name"), "hr");
assert.equal(widget.data("department-name"), "hr");
$widget.data("department-name", "hr");
assert.equal($widget.attr("data-department-name"), "hr");
assert.equal($widget.data("department-name"), "hr");
widget.html("<b>hello</b>");
assert.equal(widget.html(), "<b>hello</b>");
$widget.html("<b>hello</b>"); // eslint-disable-line no-jquery/no-parse-html-literal
assert.equal($widget.html(), "<b>hello</b>");
widget.prop("title", "My widget");
assert.equal(widget.prop("title"), "My widget");
$widget.prop("title", "My widget");
assert.equal($widget.prop("title"), "My widget");
widget.val("42");
assert.equal(widget.val(), "42");
$widget.val("42");
assert.equal($widget.val(), "42");
});
run_test("finding_related_objects", () => {
@@ -84,17 +84,17 @@ run_test("finding_related_objects", () => {
// But you can set up your tests to simulate DOM relationships.
//
// We will use set_find_results(), which is a special zjquery helper.
const emoji = $('<div class="emoji">');
$("#my-message").set_find_results(".emoji", emoji);
const $emoji = $("<emoji-stub>");
$("#my-message").set_find_results(".emoji", $emoji);
// And then calling the function produces the desired effect:
update_message_emoji("foo.png");
assert.equal(emoji.attr("src"), "foo.png");
assert.equal($emoji.attr("src"), "foo.png");
// Sometimes you want to deliberately test paths that do not find an
// element. You can pass 'false' as the result for those cases.
emoji.set_find_results(".random", false);
assert.equal(emoji.find(".random").length, 0);
$emoji.set_find_results(".random", false);
assert.equal($emoji.find(".random").length, 0);
/*
An important thing to understand is that zjquery doesn't truly
simulate DOM. The way you make relationships work in zjquery
@@ -103,12 +103,12 @@ run_test("finding_related_objects", () => {
Here is another example.
*/
const my_parents = $("#folder1,#folder4");
const elem = $("#folder555");
const $my_parents = $("#folder1,#folder4");
const $elem = $("#folder555");
elem.set_parents_result(".folder", my_parents);
elem.parents(".folder").addClass("active");
assert.ok(my_parents.hasClass("active"));
$elem.set_parents_result(".folder", $my_parents);
$elem.parents(".folder").addClass("active");
assert.ok($my_parents.hasClass("active"));
});
run_test("clicks", () => {
@@ -188,12 +188,12 @@ run_test("create", () => {
// You can create jQuery objects that aren't tied to any particular
// selector, and which just have a name.
const obj1 = $.create("the table holding employees");
const obj2 = $.create("the collection of rows in the table");
const $obj1 = $.create("the table holding employees");
const $obj2 = $.create("the collection of rows in the table");
obj1.show();
assert.ok(obj1.visible());
$obj1.show();
assert.ok($obj1.visible());
obj2.addClass(".striped");
assert.ok(obj2.hasClass(".striped"));
$obj2.addClass(".striped");
assert.ok($obj2.hasClass(".striped"));
});

View File

@@ -331,8 +331,8 @@ class CommonUtils {
return false;
}
const row = zulip_test.last_visible_row();
if (zulip_test.row_id(row) !== last_msg.id) {
const $row = zulip_test.last_visible_row();
if (zulip_test.row_id($row) !== last_msg.id) {
return false;
}
@@ -343,7 +343,7 @@ class CommonUtils {
don't add the star icon until the server
responds.
*/
return row.find(".star").length === 1;
return $row.find(".star").length === 1;
},
{},
content,
@@ -463,6 +463,16 @@ class CommonUtils {
assert.deepStrictEqual(last_n_messages, messages);
}
async open_streams_modal(page: Page): Promise<void> {
const all_streams_selector = "#add-stream-link";
await page.waitForSelector(all_streams_selector, {visible: true});
await page.click(all_streams_selector);
await page.waitForSelector("#subscription_overlay.new-style", {visible: true});
const url = await this.page_url_with_fragment(page);
assert.ok(url.includes("#streams/all"));
}
async manage_organization(page: Page): Promise<void> {
const menu_selector = "#settings-dropdown";
await page.waitForSelector(menu_selector, {visible: true});

View File

@@ -31,8 +31,8 @@ async function copy_messages(
$("body").trigger(new $.Event("keydown", {which: 67, ctrlKey: true}));
// find temp div with copied text
const temp_div = $("#copytempdiv");
return temp_div
const $temp_div = $("#copytempdiv");
return $temp_div
.children("p")
.get()
.map((p) => p.textContent!);

View File

@@ -4,10 +4,10 @@ import common from "../puppeteer_lib/common";
async function click_delete_and_return_last_msg_id(page: Page): Promise<string | undefined> {
return await page.evaluate(() => {
const msg = $("#zhome .message_row").last();
msg.find(".message_control_button.actions_hover").trigger("click");
const $msg = $("#zhome .message_row").last();
$msg.find(".message_control_button.actions_hover").trigger("click");
$(".delete_message").trigger("click");
return msg.attr("id");
return $msg.attr("id");
});
}

View File

@@ -4,8 +4,8 @@ import common from "../puppeteer_lib/common";
async function trigger_edit_last_message(page: Page): Promise<void> {
await page.evaluate(() => {
const msg = $("#zhome .message_row").last();
msg.find(".message_control_button.actions_hover").trigger("click");
const $msg = $("#zhome .message_row").last();
$msg.find(".message_control_button.actions_hover").trigger("click");
$(".popover_edit_message").trigger("click");
});
await page.waitForSelector(".message_edit_content", {visible: true});

View File

@@ -128,7 +128,7 @@ async function test_navigations_from_home(page: Page): Promise<void> {
await page.evaluate(() =>
$(
'*[title="Narrow to your private messages with Cordelia, Lear\'s daughter, King Hamlet"]',
).click(),
).trigger("click"),
);
await un_narrow_by_clicking_org_icon(page);
await expect_recent_topics(page);

View File

@@ -12,17 +12,17 @@ async function stars_count(page: Page): Promise<number> {
async function toggle_test_star_message(page: Page): Promise<void> {
await page.evaluate((message: string) => {
const msg = $(`.message_content:contains("${CSS.escape(message)}"):visible`).last();
if (msg.length !== 1) {
const $msg = $(`.message_content:contains("${CSS.escape(message)}"):visible`).last();
if ($msg.length !== 1) {
throw new Error("cannot find test star message");
}
const star_icon = msg.closest(".messagebox").find(".star");
if (star_icon.length !== 1) {
const $star_icon = $msg.closest(".messagebox").find(".star");
if ($star_icon.length !== 1) {
throw new Error("cannot find star icon");
}
star_icon.trigger("click");
$star_icon.trigger("click");
}, message);
}

View File

@@ -1,6 +1,6 @@
import {strict as assert} from "assert";
import type {ElementHandle, Page} from "puppeteer";
import type {Page} from "puppeteer";
import common from "../puppeteer_lib/common";
@@ -34,53 +34,11 @@ async function stream_name_error(page: Page): Promise<string> {
return await common.get_text_from_selector(page, "#stream_name_error");
}
async function open_streams_modal(page: Page): Promise<void> {
const all_streams_selector = "#add-stream-link";
await page.waitForSelector(all_streams_selector, {visible: true});
await page.click(all_streams_selector);
await page.waitForSelector("#subscription_overlay.new-style", {visible: true});
const url = await common.page_url_with_fragment(page);
assert.ok(url.includes("#streams/all"));
}
async function test_subscription_button(page: Page): Promise<void> {
const stream_selector = "[data-stream-name='Venice']";
const button_selector = `${stream_selector} .sub_unsub_button`;
const subscribed_selector = `${button_selector}.checked`;
const unsubscribed_selector = `${button_selector}:not(.checked)`;
async function subscribed(): Promise<ElementHandle | null> {
return await page.waitForSelector(subscribed_selector, {visible: true});
}
async function unsubscribed(): Promise<ElementHandle | null> {
return await page.waitForSelector(unsubscribed_selector, {visible: true});
}
// Make sure that Venice is even in our list of streams.
await page.waitForSelector(stream_selector, {visible: true});
await page.waitForSelector(button_selector, {visible: true});
// Note that we intentionally re-find the button after each click, since
// the live-update code may replace the whole row.
let button;
// We assume Venice is already subscribed, so the first line here
// should happen immediately.
button = await subscribed();
await button!.click();
button = await unsubscribed();
await button!.click();
button = await subscribed();
await button!.click();
button = await unsubscribed();
await button!.click();
button = await subscribed();
}
async function click_create_new_stream(page: Page): Promise<void> {
await page.click("#add_new_subscription .create_stream_button");
await page.waitForSelector(".finalize_create_stream", {visible: true});
// sanity check that desdemona is the initial subsscriber
await await_user_visible(page, "desdemona");
}
@@ -205,8 +163,7 @@ async function test_streams_search_feature(page: Page): Promise<void> {
async function subscriptions_tests(page: Page): Promise<void> {
await common.log_in(page);
await open_streams_modal(page);
await test_subscription_button(page);
await common.open_streams_modal(page);
await test_stream_creation(page);
await test_streams_search_feature(page);
}

View File

@@ -0,0 +1,54 @@
import type {ElementHandle, Page} from "puppeteer";
import common from "../puppeteer_lib/common";
async function test_subscription_button(page: Page): Promise<void> {
const stream_selector = "[data-stream-name='Venice']";
const button_selector = `${stream_selector} .sub_unsub_button`;
const subscribed_selector = `${button_selector}.checked`;
const unsubscribed_selector = `${button_selector}:not(.checked)`;
async function subscribed(): Promise<ElementHandle | null> {
return await page.waitForSelector(subscribed_selector, {visible: true});
}
async function unsubscribed(): Promise<ElementHandle | null> {
return await page.waitForSelector(unsubscribed_selector, {visible: true});
}
// Make sure that Venice is even in our list of streams.
await page.waitForSelector(stream_selector, {visible: true});
await page.waitForSelector(button_selector, {visible: true});
// Note that we intentionally re-find the button after each click, since
// the live-update code may replace the whole row.
let button;
// We assume Venice is already subscribed, so the first line here
// should happen immediately.
button = await subscribed();
// Toggle subscriptions several times. This test code has been known
// to flake, so we add console statements. It appears that the console
// statements help prevent the flake, which is probably caused by some
// subtle race condition. We will hopefully diagnose the root cause of
// the flake, but I am confident that the test will mostly catch actual
// bugs in the real code, so as long as the console.info statements help
// here, we should just leave them in.
for (let i = 0; i < 10; i += 1) {
console.info(`\n unsubscribe/subscribe loop ${i}\n\n`);
await button!.click();
button = await unsubscribed();
await button!.click();
button = await subscribed();
console.info(`\n end loop ${i}\n\n`);
}
}
async function subscriptions_tests(page: Page): Promise<void> {
await common.log_in(page);
await common.open_streams_modal(page);
await test_subscription_button(page);
}
common.run_test(subscriptions_tests);

View File

@@ -18,6 +18,23 @@ async function user_row(page: Page, name: string): Promise<string> {
return `.user_row[data-user-id="${CSS.escape(user_id.toString())}"]`;
}
async function test_reactivation_confirmation_modal(page: Page, fullname: string): Promise<void> {
await common.wait_for_micromodal_to_open(page);
assert.strictEqual(
await common.get_text_from_selector(page, ".dialog_heading"),
"Reactivate " + fullname,
"Reactivate modal has wrong user.",
);
assert.strictEqual(
await common.get_text_from_selector(page, "#dialog_widget_modal .dialog_submit_button"),
"Confirm",
"Reactivate button has incorrect text.",
);
await page.click("#dialog_widget_modal .dialog_submit_button");
await common.wait_for_micromodal_to_close(page);
}
async function test_deactivate_user(page: Page): Promise<void> {
const cordelia_user_row = await user_row(page, "cordelia");
await page.waitForSelector(cordelia_user_row, {visible: true});
@@ -45,10 +62,11 @@ async function test_reactivate_user(page: Page): Promise<void> {
await page.waitForSelector(cordelia_user_row + " .fa-user-plus");
await page.click(cordelia_user_row + " .reactivate");
await test_reactivation_confirmation_modal(page, common.fullname.cordelia);
await page.waitForSelector(cordelia_user_row + ":not(.deactivated_user)", {visible: true});
cordelia_user_row = await user_row(page, "cordelia");
await page.waitForSelector(cordelia_user_row + " .fa-user-times");
await page.waitForSelector("#user-field-status", {hidden: true});
}
async function test_deactivated_users_section(page: Page): Promise<void> {
@@ -66,6 +84,9 @@ async function test_deactivated_users_section(page: Page): Promise<void> {
{visible: true},
);
await page.click("#admin_deactivated_users_table " + cordelia_user_row + " .reactivate");
await test_reactivation_confirmation_modal(page, common.fullname.cordelia);
await page.waitForSelector(
"#admin_deactivated_users_table " + cordelia_user_row + " button:not(.reactivate)",
{visible: true},
@@ -83,6 +104,8 @@ async function test_bot_deactivation_and_reactivation(page: Page): Promise<void>
await page.waitForSelector("#bot-field-status", {hidden: true});
await page.click(default_bot_user_row + " .reactivate");
await test_reactivation_confirmation_modal(page, "Zulip Default Bot");
await page.waitForSelector(default_bot_user_row + ":not(.deactivated_user)", {visible: true});
await page.waitForSelector(default_bot_user_row + " .fa-user-times");
}

View File

@@ -188,7 +188,7 @@ exports.mock_cjs = (module_path, obj) => {
};
exports.mock_jquery = ($) => {
jquery_function = $;
jquery_function = $; // eslint-disable-line no-jquery/variable-pattern
return $;
};

View File

@@ -43,8 +43,8 @@ function make_zjquery() {
const fn = {};
function new_elem(selector, create_opts) {
const elem = FakeElement(selector, {...create_opts});
Object.assign(elem, fn);
const $elem = FakeElement(selector, {...create_opts});
Object.assign($elem, fn);
// Create a proxy handler to detect missing stubs.
//
@@ -59,7 +59,7 @@ function make_zjquery() {
if (key === "stack") {
const error =
"\nInstead of doing equality checks on a full object, " +
'do `assert_equal(foo.selector, ".some_class")\n';
'do `assert_equal($foo.selector, ".some_class")\n';
throw new Error(error);
}
@@ -76,7 +76,7 @@ function make_zjquery() {
},
};
const proxy = new Proxy(elem, handler);
const proxy = new Proxy($elem, handler);
return proxy;
}
@@ -130,8 +130,8 @@ function make_zjquery() {
verify_selector_for_zulip(selector);
if (!elems.has(selector)) {
const elem = new_elem(selector);
elems.set(selector, elem);
const $elem = new_elem(selector);
elems.set(selector, $elem);
}
return elems.get(selector);
};
@@ -146,10 +146,10 @@ function make_zjquery() {
zjquery.create = function (name, opts) {
assert.ok(!elems.has(name), "You already created an object with this name!!");
const elem = new_elem(name, opts);
elems.set(name, elem);
const $elem = new_elem(name, opts);
elems.set(name, $elem);
return elem;
return $elem;
};
zjquery.trim = function (s) {
@@ -158,7 +158,7 @@ function make_zjquery() {
zjquery.state = function () {
// useful for debugging
let res = Array.from(elems.values(), (v) => v.debug());
let res = Array.from(elems.values(), ($v) => $v.debug());
res = res.map((v) => [v.selector, v.value, v.shown]);
@@ -250,4 +250,4 @@ const $ = new Proxy(make_zjquery(), {
},
});
module.exports = $;
module.exports = $; // eslint-disable-line no-jquery/variable-pattern

View File

@@ -15,38 +15,38 @@ function FakeElement(selector, opts) {
let height;
const find_results = new Map();
let my_parent;
let $my_parent;
const parents_result = new Map();
const properties = new Map();
const attrs = new Map();
const classes = new Map();
const event_store = make_event_store(selector);
const self = {
const $self = {
addClass(class_name) {
classes.set(class_name, true);
return self;
return $self;
},
append(arg) {
html = html + arg;
return self;
return $self;
},
attr(name, val) {
if (val === undefined) {
return attrs.get(name);
}
attrs.set(name, val);
return self;
return $self;
},
data(name, val) {
if (val === undefined) {
return attrs.get("data-" + name);
}
attrs.set("data-" + name, val);
return self;
return $self;
},
delay() {
return self;
return $self;
},
debug() {
return {
@@ -59,22 +59,22 @@ function FakeElement(selector, opts) {
if (arg === undefined) {
find_results.clear();
}
return self;
return $self;
},
eq() {
return self;
return $self;
},
expectOne() {
// silently do nothing
return self;
return $self;
},
fadeTo: noop,
find(child_selector) {
const child = find_results.get(child_selector);
if (child) {
return child;
const $child = find_results.get(child_selector);
if ($child) {
return $child;
}
if (child === false) {
if ($child === false) {
// This is deliberately set to simulate missing find results.
// Return an empty array, the most common check is
// if ($.find().length) { //success }
@@ -84,12 +84,12 @@ function FakeElement(selector, opts) {
We need you to simulate the results of $(...).find(...)
by using set_find_results. You want something like this:
const container = ...;
const child = ...;
container.set_find_results("${child_selector}", child);
const $container = ...;
const $child = ...;
$container.set_find_results("${child_selector}", $child);
Then calling container.find("${child_selector}") will return
the "child" zjquery element.
Then calling $container.find("${child_selector}") will return
the "$child" zjquery element.
`);
},
@@ -107,12 +107,12 @@ function FakeElement(selector, opts) {
},
hide() {
shown = false;
return self;
return $self;
},
html(arg) {
if (arg !== undefined) {
html = arg;
return self;
return $self;
}
return html;
},
@@ -121,9 +121,9 @@ function FakeElement(selector, opts) {
return shown;
}
if (arg === ":focus") {
return self.is_focused();
return $self.is_focused();
}
return self;
return $self;
},
is_focused() {
// is_focused is not a jQuery thing; this is
@@ -132,7 +132,7 @@ function FakeElement(selector, opts) {
},
off(...args) {
event_store.off(...args);
return self;
return $self;
},
offset() {
return {
@@ -142,44 +142,44 @@ function FakeElement(selector, opts) {
},
on(...args) {
event_store.on(...args);
return self;
return $self;
},
one(...args) {
event_store.one(...args);
return self;
return $self;
},
parent() {
return my_parent;
return $my_parent;
},
parents(parents_selector) {
const result = parents_result.get(parents_selector);
const $result = parents_result.get(parents_selector);
assert.ok(
result,
$result,
"You need to call set_parents_result for " + parents_selector + " in " + selector,
);
return result;
return $result;
},
prepend(arg) {
html = arg + html;
return self;
return $self;
},
prop(name, val) {
if (val === undefined) {
return properties.get(name);
}
properties.set(name, val);
return self;
return $self;
},
removeAttr(name) {
attrs.delete(name);
return self;
return $self;
},
removeClass(class_names) {
class_names = class_names.split(" ");
for (const class_name of class_names) {
classes.delete(class_name);
}
return self;
return $self;
},
remove() {
throw new Error(`
@@ -193,68 +193,68 @@ function FakeElement(selector, opts) {
},
removeData: noop,
replaceWith() {
return self;
return $self;
},
scrollTop() {
return self;
return $self;
},
serializeArray() {
return self;
return $self;
},
set_find_results(find_selector, jquery_object) {
if (jquery_object === undefined) {
set_find_results(find_selector, $jquery_object) {
if ($jquery_object === undefined) {
throw new Error(
"Please make the 'find result' be something like $.create('unused')",
);
}
find_results.set(find_selector, jquery_object);
find_results.set(find_selector, $jquery_object);
},
set_height(fake_height) {
height = fake_height;
},
set_parent(parent_elem) {
my_parent = parent_elem;
set_parent($parent_elem) {
$my_parent = $parent_elem;
},
set_parents_result(selector, result) {
parents_result.set(selector, result);
set_parents_result(selector, $result) {
parents_result.set(selector, $result);
},
show() {
shown = true;
return self;
return $self;
},
slice() {
return self;
return $self;
},
stop() {
return self;
return $self;
},
text(...args) {
if (args.length !== 0) {
if (args[0] !== undefined) {
text = args[0].toString();
}
return self;
return $self;
}
return text;
},
toggle(show) {
assert.ok([true, false].includes(show));
shown = show;
return self;
return $self;
},
tooltip() {
return self;
return $self;
},
trigger(ev) {
event_store.trigger(self, ev);
return self;
event_store.trigger($self, ev);
return $self;
},
val(...args) {
if (args.length === 0) {
return value || "";
}
[value] = args;
return self;
return $self;
},
visible() {
return shown;
@@ -262,34 +262,34 @@ function FakeElement(selector, opts) {
};
if (opts.children) {
self.map = (f) => opts.children.map((i, elem) => f(elem, i));
self.each = (f) => {
$self.map = (f) => opts.children.map((i, elem) => f(elem, i));
$self.each = (f) => {
for (const child of opts.children) {
f.call(child);
}
};
self[Symbol.iterator] = function* () {
$self[Symbol.iterator] = function* () {
for (const child of opts.children) {
yield child;
}
};
for (const [i, child] of opts.children.entries()) {
self[i] = child;
$self[i] = child;
}
self.length = opts.children.length;
$self.length = opts.children.length;
}
if (selector[0] === "<") {
self.html(selector);
$self.html(selector);
}
self.selector = selector;
$self.selector = selector;
self.__zjquery = true;
$self.__zjquery = true;
return self;
return $self;
}
function make_event_store(selector) {
@@ -394,7 +394,8 @@ function make_event_store(selector) {
ev = new FakeEvent(ev);
}
if (!ev.target) {
ev.target = $element;
// FIXME: event.target should not be a jQuery object
ev.target = $element; // eslint-disable-line no-jquery/variable-pattern
}
const func = on_functions.get(ev.type);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More