Commit Graph

218 Commits

Author SHA1 Message Date
Sara Gulotta 1cb040647b markdown: Add support for spoilers.
This adds support for a "spoiler" syntax in Zulip's markdown, which
can be used to hide content that one doesn't want to be immediately
visible without a click.

We use our own spoiler block syntax inspired by Zulip's existing quote
and math block markdown extensions, rather than requiring a token on
every line, as is present in some other markdown spoiler
implementations.

Fixes #5802.

Co-authored-by: Dylan Nugent <dylnuge@gmail.com>
2020-06-16 16:14:10 -07:00
Steve Howell b965766f87 refactor: Call compose_fade without triggers.
We are trying to phase out the trigger-event way
of telling modules to do something.

In this case we not only remove the indirection
of the event handler, but we also get to remove
`compose_fade` from the `ui_init` startup sequence.

This also has us update `compose_fade` outside
the loop, although that's only a theoretical
improvement, since I don't think `peer_add` events
every actually include multiple streams.

To make the dispatch tests a little flatter, I
added a one-line change to zjsunit to add
`make_stub` to `global`.

To manually test:
    * have Aaron reply to Denmark (keep compose box open)
    * have Iago add Hamlet to Denmark
    * have Hamlet unsubscribe
2020-06-13 15:35:39 -07:00
Aman Agrawal 9f50825610 recent_topics: Only rerender when recent_topics is visible.
We update the data in the background but only update the view
when recent topics is visible to user.
Also, we always do a complete rerender on launch.
2020-06-09 22:08:31 -07:00
Aman Agrawal 464b541363 recent_topics: Display recent topics in a table.
* Add action to mute topics.
* We don't need to store muted data per topic as previously planned.
* Moved launch topic test to the top so that they run on non-modified
  data.
2020-06-09 22:08:31 -07:00
Anders Kaseorg aa912ef759 mdiff: Convert formatter, aliases from object to Map.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-26 23:33:40 -07:00
Anders Kaseorg 0423e6b7cc zjsunit: Remove pointless reassignment of ‘module’.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-26 23:33:40 -07:00
Rohitt Vashishtha 84b2952606 zjquery: Support empty find results. 2020-05-21 12:32:52 -07:00
Rohitt Vashishtha c76648c3d4 zjquery: Do not run JSON.parse on data-* attrs.
JSON.parse behaves as we want for numbers but for strings, we would
throw an error like 'unexpected token at position 0'. This meant we
couldn't read back the value set by `$input.data('val', 'text')`.
2020-05-16 14:51:19 -07:00
Steve Howell d9166e2ab1 zblueslip: Simplify broken error message.
We were reporting 1 of 1 when 0 messages were
seen.
2020-04-24 18:24:12 -04:00
Rohitt Vashishtha 41481a906c settings: Simplify settings_list_widget interface.
We move more common code into settings_lsit_widget for cleaner code
in settings_org.js.
2020-04-22 17:57:16 -07:00
Rohitt Vashishtha c8b0627ffe zblueslip: Run blueslip.reset after each test.
This simplifies the blueslip interface to just declaring expected
errors and calling the code to test.
2020-04-20 08:17:20 -04:00
Rohitt Vashishtha 7c357b27d8 zblueslip: Do not expose internal function check_seen_messages. 2020-04-20 08:17:20 -04:00
Rohitt Vashishtha b2c55da3e1 zblueslip: Remove now redundant seen_messages tracking. 2020-04-20 08:17:20 -04:00
Rohitt Vashishtha 0def4a97ae zblueslip: Implement tracking extra/lesser blueslip calls.
We change the user facing interface to allow specifying expected
number of error messages (default=1). Now an average test can look
like:

```
    // We expect 3 error messages;
    blueslip.expect('error', 'an error message', 3);
    throwError();
    throwError();
    throwError();
    blueslip.reset();
```
2020-04-20 08:17:20 -04:00
Steve Howell 9943a07e8c node tests: Improve handling of blueslip.fatal().
We now use `assert.throws()` to test that we're
properly calling `blueslip.fatal`.

In order to not break line coverage here, we have
to remove an unreachable `return` in `stream_data.js`.

Usually we test `fatal` for line coverage reasons.
Most places where we use `blueslip.fatal` fall in
these categories:

    * the code is theoretically unreachable, but
      we have `blueslip.fatal` for defensive reasons

    * we have some upstream bug that we should just
      fix

    * the code should recover gracefully and just
      use blueslip.errors()

It's possible that we should eliminate `blueslip.fatal`
from our API and just throw errors when really important
invariants get broken.  This will make it more obvious
to somebody reading the code that we're not going to
continue after the call, and `blueslip` already knows
how to catch exceptions and report them.
2020-04-08 11:37:27 -04:00
Steve Howell f7b432afec node tests: Auto-include zblueslip for node tests.
We already use blueslip stubs in ~45 tests, so we
may as well just auto-include it.
2020-04-03 12:56:49 -04:00
Steve Howell df84c52a7f zblueslip: Change API to expect/reset.
The `set_test_data` never made complete sense to
me, since it wasn't really data that we were
setting.
2020-04-03 12:56:49 -04:00
Steve Howell ec2aaa52dd zblueslip: Prevent spurious expected errors.
This also cleans up some idioms in the zblueslip
code.
2020-04-03 12:56:49 -04:00
Steve Howell 26baaab34c zblueslip: Expect strings in blueslip calls. 2020-04-03 12:56:49 -04:00
Steve Howell 95b84c0057 zblueslip: Remove unused check_error(). 2020-04-03 12:56:49 -04:00
Stefan Weil d2fa058cc1
text: Fix some typos (most of them found and fixed by codespell).
Signed-off-by: Stefan Weil <sw@weilnetz.de>
2020-03-27 17:25:56 -07:00
Steve Howell b994889315 node tests: Just set i18n every time.
Explicitly stubbing i18n in 48 different files
is mostly busy work at this point, and it doesn't
provide much signal, since often it's invoked
only to satisfy transitive dependencies.
2020-02-28 17:11:24 -08:00
Steve Howell 216493aae8 zjsunit: Clear namespace more aggressively.
Let's say you have module hello.js like so:

    // hello.js
    const hello_world = i18n.t('Hello world');
    exports.get_greeting = () => hello_world;

And then two modules like this:

    // apple.js
    const hello = require('hello');

    exports.foo = () => {
        show_greeting(hello.get_greeting());
    };

    // banana.js
    const hello = require('hello');

    exports.foo = () => {
        display_greeting(hello.get_greeting());
    };

The test for apple.js could look like this,
and it won't crash due to the stub:

    set_global('i18n', {t: () => {}});
    zrequire('hello');
    zrequire('apple');

Now let's say your write this broken version
of a test for banana.js:

    zrequire('hello');
    zrequire('banana');

If you run `./tools/test-js-with-node`, the
"banana" test will pass, because while it
does require "hello", it won't actually
*execute* the code that happens at require
time for "hello", because it's already in
the cache.  Here is the code that gets
skipped:

    const hello_world = i18n.t('Hello world');

But then if you try to run the banana test
individually, the above line of code will
cause the test to crash.  And it will crash
even before you actually try to test the
meaningful code here:

    exports.foo = () => {
        display_greeting(hello.get_greeting());
    };

This commit fixes this leak scenario by just
aggressively clearing out things from the
require cache.

This slows tests down by about 10%, which I think
is worth the extra safety here.
2020-02-27 10:21:36 -05:00
Anders Kaseorg c9dbd13189 js: Convert _.has to Object.prototype.hasOwnProperty.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-25 14:09:39 -08:00
Anders Kaseorg dbffb2a614 js: Convert _.extend to spread syntax or Object.assign.
This is not always a behavior-preserving translation: _.extend mutates
its first argument.  However, the code does not always appear to have
been written to expect that.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-25 14:09:39 -08:00
Anders Kaseorg 6f32ef749f js: Convert $.extend to spread syntax.
This is not always a behavior-preserving translation: $.extend mutates
its first argument.  However, the code does not always appear to have
been written to expect that.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-25 14:09:39 -08:00
Anders Kaseorg 4889a0486d tests: Compile Handlebars templates with source maps.
This allows us to collect coverage for Handlebars templates, and also
improves the readability of Handlebars-related stack traces.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-18 07:38:46 -05:00
Anders Kaseorg f8bf0f4c49 zjquery: Convert elems from object to Map.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-12 10:39:01 -08:00
Anders Kaseorg 8892f463a8 zjquery: Fix zjquery.state.
Fixes an incorrect translation from _.map in #13850.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-12 10:39:01 -08:00
Anders Kaseorg 72dddb7af6 zjsunit: Use assert in strict mode.
This makes assert.equal and assert.deepEqual compare using === rather
than ==, to catch more bugs.

https://nodejs.org/api/assert.html#assert_strict_mode

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-12 08:16:26 -05:00
Anders Kaseorg d16a730d81 zjquery: Mock text more accurately.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-12 08:16:26 -05:00
Anders Kaseorg 59d55d1e06 js: Use modern spread arguments syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-11 17:43:35 -08:00
Steve Howell 795c3ad8a5 zjquery: Remove closest() implementation.
This effectively reverts the following
commit from May 2019:

be527905ca

The implementation of closest() was a bit
buggy and complex.  It's easy enough
to just stub the method yourself.  We may
want to eventually re-implement it, but we
should follow the template of parent/set_parent.

If you fail to stub `closest` zjquery gives
a fairly helpful error message:

Error: You must create a stub for $("link-stub").closest
2020-02-11 14:19:03 -05:00
Anders Kaseorg d0b0a64af3 js: Convert _.isEmpty(a) to a.length !== 0.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-10 14:08:12 -08:00
Anders Kaseorg e1cf5e4630 js: Convert _.reduce to less convoluted code.
reduce is almost never a better solution than the alternatives.  Avoid
it.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-10 14:08:12 -08:00
Anders Kaseorg b566d11d69 js: Convert _.findIndex(a, …) to a.findIndex(…).
And convert the corresponding function expressions to arrow style
while we’re here.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-10 14:08:12 -08:00
Anders Kaseorg ac7b09d57e js: Convert _.map(a, …) to a.map(…).
And convert the corresponding function expressions to arrow style
while we’re here.

import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";

const checkExpression = (node: n.Node): node is K.ExpressionKind =>
  n.Expression.check(node);

for (const file of process.argv.slice(2)) {
  console.log("Parsing", file);
  const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
    parser: path.extname(file) === ".ts" ? tsParser : babelParser,
  });
  let changed = false;

  recast.visit(ast, {
    visitCallExpression(path) {
      const { callee, arguments: args } = path.node;
      if (
        n.MemberExpression.check(callee) &&
        !callee.computed &&
        n.Identifier.check(callee.object) &&
        callee.object.name === "_" &&
        n.Identifier.check(callee.property) &&
        callee.property.name === "map" &&
        args.length === 2 &&
        checkExpression(args[0]) &&
        checkExpression(args[1])
      ) {
        const [arr, fn] = args;
        path.replace(
          b.callExpression(b.memberExpression(arr, b.identifier("map")), [
            n.FunctionExpression.check(fn) ||
            n.ArrowFunctionExpression.check(fn)
              ? b.arrowFunctionExpression(
                  fn.params,
                  n.BlockStatement.check(fn.body) &&
                    fn.body.body.length === 1 &&
                    n.ReturnStatement.check(fn.body.body[0])
                    ? fn.body.body[0].argument || b.identifier("undefined")
                    : fn.body
                )
              : fn,
          ])
        );
        changed = true;
      }
      this.traverse(path);
    },
  });

  if (changed) {
    console.log("Writing", file);
    fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
  }
}

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-10 14:08:12 -08:00
Anders Kaseorg 719546641f js: Convert a.indexOf(…) !== -1 to a.includes(…).
Babel polyfills this for us for Internet Explorer.

import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";

const checkExpression = (node: n.Node): node is K.ExpressionKind =>
  n.Expression.check(node);

for (const file of process.argv.slice(2)) {
  console.log("Parsing", file);
  const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
    parser: path.extname(file) === ".ts" ? tsParser : babelParser,
  });
  let changed = false;

  recast.visit(ast, {
    visitBinaryExpression(path) {
      const { operator, left, right } = path.node;
      if (
        n.CallExpression.check(left) &&
        n.MemberExpression.check(left.callee) &&
        !left.callee.computed &&
        n.Identifier.check(left.callee.property) &&
        left.callee.property.name === "indexOf" &&
        left.arguments.length === 1 &&
        checkExpression(left.arguments[0]) &&
        ((["===", "!==", "==", "!=", ">", "<="].includes(operator) &&
          n.UnaryExpression.check(right) &&
          right.operator == "-" &&
          n.Literal.check(right.argument) &&
          right.argument.value === 1) ||
          ([">=", "<"].includes(operator) &&
            n.Literal.check(right) &&
            right.value === 0))
      ) {
        const test = b.callExpression(
          b.memberExpression(left.callee.object, b.identifier("includes")),
          [left.arguments[0]]
        );
        path.replace(
          ["!==", "!=", ">", ">="].includes(operator)
            ? test
            : b.unaryExpression("!", test)
        );
        changed = true;
      }
      this.traverse(path);
    },
  });

  if (changed) {
    console.log("Writing", file);
    fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
  }
}

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-10 14:08:12 -08:00
Anders Kaseorg 11b5d80800 tests: Fix more undefined mocks.
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2020-02-10 14:08:12 -08:00
Steve Howell e9c6653852 node tests: Always enforce blueslip warn/error/fatal.
We now require all of our unit tests to handle
blueslip errors for warn/error/fatal.  This
simplifies the zblueslip code to not have any
options passed in.

Most of the places changed here fell into two
categories:

    - We were just missing a random piece of
      setup data in a happy path test.

    - We were testing error handling in just
      a lazy way to ensure 100% coverage.  Often
      these error codepaths were fairly
      contrived.

The one place where we especially lazy was
the stream_data tests, and those are now
more thorough.
2020-02-07 14:15:44 -08:00
Anders Kaseorg 02511bff1c js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.

import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";

const checkExpression = (node: n.Node): node is K.ExpressionKind =>
  n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
  n.Statement.check(node);

for (const file of process.argv.slice(2)) {
  console.log("Parsing", file);
  const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
    parser: path.extname(file) === ".ts" ? tsParser : babelParser,
  });
  let changed = false;
  let inLoop = false;
  let replaceReturn = false;

  const visitLoop = (...args: string[]) =>
    function(this: Context, path: NodePath) {
      for (const arg of args) {
        this.visit(path.get(arg));
      }
      const old = { inLoop };
      inLoop = true;
      this.visit(path.get("body"));
      inLoop = old.inLoop;
      return false;
    };

  recast.visit(ast, {
    visitDoWhileStatement: visitLoop("test"),

    visitExpressionStatement(path) {
      const { expression, comments } = path.node;
      let valueOnly;
      if (
        n.CallExpression.check(expression) &&
        n.MemberExpression.check(expression.callee) &&
        !expression.callee.computed &&
        n.Identifier.check(expression.callee.object) &&
        expression.callee.object.name === "_" &&
        n.Identifier.check(expression.callee.property) &&
        ["each", "forEach"].includes(expression.callee.property.name) &&
        [2, 3].includes(expression.arguments.length) &&
        checkExpression(expression.arguments[0]) &&
        (n.FunctionExpression.check(expression.arguments[1]) ||
          n.ArrowFunctionExpression.check(expression.arguments[1])) &&
        [1, 2].includes(expression.arguments[1].params.length) &&
        n.Identifier.check(expression.arguments[1].params[0]) &&
        ((valueOnly = expression.arguments[1].params[1] === undefined) ||
          n.Identifier.check(expression.arguments[1].params[1])) &&
        (expression.arguments[2] === undefined ||
          n.ThisExpression.check(expression.arguments[2]))
      ) {
        const old = { inLoop, replaceReturn };
        inLoop = false;
        replaceReturn = true;
        this.visit(
          path
            .get("expression")
            .get("arguments")
            .get(1)
            .get("body")
        );
        inLoop = old.inLoop;
        replaceReturn = old.replaceReturn;

        const [right, { body, params }] = expression.arguments;
        const loop = b.forOfStatement(
          b.variableDeclaration("let", [
            b.variableDeclarator(
              valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
            ),
          ]),
          valueOnly
            ? right
            : b.callExpression(
                b.memberExpression(right, b.identifier("entries")),
                []
              ),
          checkStatement(body) ? body : b.expressionStatement(body)
        );
        loop.comments = comments;
        path.replace(loop);
        changed = true;
      }
      this.traverse(path);
    },

    visitForStatement: visitLoop("init", "test", "update"),

    visitForInStatement: visitLoop("left", "right"),

    visitForOfStatement: visitLoop("left", "right"),

    visitFunction(path) {
      this.visit(path.get("params"));
      const old = { replaceReturn };
      replaceReturn = false;
      this.visit(path.get("body"));
      replaceReturn = old.replaceReturn;
      return false;
    },

    visitReturnStatement(path) {
      if (replaceReturn) {
        assert(!inLoop); // could use labeled continue if this ever fires
        const { argument, comments } = path.node;
        if (argument === null) {
          const s = b.continueStatement();
          s.comments = comments;
          path.replace(s);
        } else {
          const s = b.expressionStatement(argument);
          s.comments = comments;
          path.replace(s, b.continueStatement());
        }
        return false;
      }
      this.traverse(path);
    },

    visitWhileStatement: visitLoop("test"),
  });

  if (changed) {
    console.log("Writing", file);
    fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
  }
}

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-07 14:09:47 -08:00
Anders Kaseorg 19f7c6f012 zjsunit: Replace add_extensions with Object.assign.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-07 14:09:47 -08:00
Anders Kaseorg 940ff9e95f zjsunit: Use modern spread arguments syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-07 14:09:47 -08:00
Steve Howell 0aa9decd86 blueslip: Add feature to time common operations.
This is relatively unobtrusive, and we don't send
anything to the server.

But any user can now enter blueslip.timings in the
console to see a map of how long things take in
milliseconds.  We only record one timing per
event label (i.e. the most recent).

It's pretty easy to test this by just clicking
around.  For 300 users/streams most things are
fast except for:

    - initialize_everything
    - manage streams (render_subscriptions)

Both do lots of nontrivial work, although
"manage streams" is a bit surprising, since
we're only measuring how long to build the
HTML from the templates (whereas the real
time is probably browser rendering costs).
2020-01-15 12:01:16 -08:00
Steve Howell 493afcb9f0 zjsquery: Add data support.
Before this we just noop'ed it, since at one time
we were trying to deprecate this is in favor
of attr calls.
2020-01-05 12:28:37 -08:00
Steve Howell 30ad1b6f16 zjsunit: Remove Dict dependency.
We now require the actual tests to explicitly
to zrequire Dict, rather than magically adding this.

In one case, the use of Dict was clearly just for
the test (not the app), so I converted that an ordinary
JS object (see timerender.js).
2020-01-03 17:19:59 -08:00
Steve Howell d41f714eff comments: Update comment for zjsunit/i18n.js. 2020-01-03 17:19:59 -08:00
Steve Howell 897320b2c4 zjquery: Use Map instead of Dict.
This seems to speed up the whole test suite
by about 20%, although measurements are a bit
noisy.
2020-01-03 17:19:59 -08:00
Anders Kaseorg 428956c086 zjsunit: Remove set_global side effect from zrequire.
ES6 and TS modules don’t insert themselves into `window`, so our tests
shouldn’t insert them either.  Since the test `window` behaves like
`global` now, we can rely on legacy modules that do insert themselves
to do it themselves.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2019-11-13 14:29:17 -08:00
Anders Kaseorg 99563eb150 zjsunit: Make window a Proxy for global.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2019-11-13 14:27:13 -08:00