about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorYamagishi Kazutoshi <ykzts@desire.sh>2017-10-16 16:33:08 +0900
committerEugen Rochko <eugen@zeonfederated.com>2017-10-16 09:33:08 +0200
commitd5b767c3747b9e7f9afcbcecffb662843ca2a346 (patch)
treebb88336c5bdce5b09358eefc310f9b098e81bfde /app/javascript
parent93b54b8d4b51f87c6e9cf642d5f57f557e9cd555 (diff)
Replace JavaScript Testing Framework from Mocha to Jest (#5412)
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap33
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap24
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap114
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap23
-rw-r--r--app/javascript/mastodon/components/__tests__/avatar-test.js36
-rw-r--r--app/javascript/mastodon/components/__tests__/avatar_overlay-test.js29
-rw-r--r--app/javascript/mastodon/components/__tests__/button-test.js75
-rw-r--r--app/javascript/mastodon/components/__tests__/display_name-test.js18
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji-test.js61
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js130
-rw-r--r--app/javascript/mastodon/features/ui/components/__tests__/column-test.js34
-rw-r--r--app/javascript/mastodon/test_setup.js5
12 files changed, 582 insertions, 0 deletions
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
new file mode 100644
index 000000000..76ab3374a
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
+<div
+  className="account__avatar"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/animated/alice.gif)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
+
+exports[`<Avatar /> Still renders a still avatar 1`] = `
+<div
+  className="account__avatar"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  style={
+    Object {
+      "backgroundImage": "url(/static/alice.jpg)",
+      "backgroundSize": "100px 100px",
+      "height": "100px",
+      "width": "100px",
+    }
+  }
+/>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
new file mode 100644
index 000000000..d59fee42f
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<AvatarOverlay renders a overlay avatar 1`] = `
+<div
+  className="account__avatar-overlay"
+>
+  <div
+    className="account__avatar-overlay-base"
+    style={
+      Object {
+        "backgroundImage": "url(/static/alice.jpg)",
+      }
+    }
+  />
+  <div
+    className="account__avatar-overlay-overlay"
+    style={
+      Object {
+        "backgroundImage": "url(/static/eve.jpg)",
+      }
+    }
+  />
+</div>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
new file mode 100644
index 000000000..c3f018d90
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
@@ -0,0 +1,114 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
+<button
+  className="button button-secondary"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a button element 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
+<button
+  className="button"
+  disabled={true}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders class="button--block" if props.block given 1`] = `
+<button
+  className="button button--block"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+/>
+`;
+
+exports[`<Button /> renders the children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  <p>
+    children
+  </p>
+</button>
+`;
+
+exports[`<Button /> renders the given text 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
+
+exports[`<Button /> renders the props.text instead of children 1`] = `
+<button
+  className="button"
+  disabled={undefined}
+  onClick={[Function]}
+  style={
+    Object {
+      "height": "36px",
+      "lineHeight": "36px",
+      "padding": "0 16px",
+    }
+  }
+>
+  foo
+</button>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
new file mode 100644
index 000000000..533359ffe
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/display_name-test.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<DisplayName /> renders display name + account name 1`] = `
+<span
+  className="display-name"
+>
+  <strong
+    className="display-name__html"
+    dangerouslySetInnerHTML={
+      Object {
+        "__html": "<p>Foo</p>",
+      }
+    }
+  />
+   
+  <span
+    className="display-name__account"
+  >
+    @
+    bar@baz
+  </span>
+</span>
+`;
diff --git a/app/javascript/mastodon/components/__tests__/avatar-test.js b/app/javascript/mastodon/components/__tests__/avatar-test.js
new file mode 100644
index 000000000..dd3f7b7d2
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar-test.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import Avatar from '../avatar';
+
+describe('<Avatar />', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const size     = 100;
+
+  describe('Autoplay', () => {
+    it('renders a animated avatar', () => {
+      const component = renderer.create(<Avatar account={account} animate size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  describe('Still', () => {
+    it('renders a still avatar', () => {
+      const component = renderer.create(<Avatar account={account} size={size} />);
+      const tree      = component.toJSON();
+
+      expect(tree).toMatchSnapshot();
+    });
+  });
+
+  // TODO add autoplay test if possible
+});
diff --git a/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
new file mode 100644
index 000000000..44addea83
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/avatar_overlay-test.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS } from 'immutable';
+import AvatarOverlay from '../avatar_overlay';
+
+describe('<AvatarOverlay', () => {
+  const account = fromJS({
+    username: 'alice',
+    acct: 'alice',
+    display_name: 'Alice',
+    avatar: '/animated/alice.gif',
+    avatar_static: '/static/alice.jpg',
+  });
+
+  const friend = fromJS({
+    username: 'eve',
+    acct: 'eve@blackhat.lair',
+    display_name: 'Evelyn',
+    avatar: '/animated/eve.gif',
+    avatar_static: '/static/eve.jpg',
+  });
+
+  it('renders a overlay avatar', () => {
+    const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/button-test.js b/app/javascript/mastodon/components/__tests__/button-test.js
new file mode 100644
index 000000000..160cd3cbc
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/button-test.js
@@ -0,0 +1,75 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Button from '../button';
+
+describe('<Button />', () => {
+  it('renders a button element', () => {
+    const component = renderer.create(<Button />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the given text', () => {
+    const text      = 'foo';
+    const component = renderer.create(<Button text={text} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('handles click events using the given handler', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(1);
+  });
+
+  it('does not handle click events if props.disabled given', () => {
+    const handler = jest.fn();
+    const button  = shallow(<Button onClick={handler} disabled />);
+    button.find('button').simulate('click');
+
+    expect(handler.mock.calls.length).toEqual(0);
+  });
+
+  it('renders a disabled attribute if props.disabled given', () => {
+    const component = renderer.create(<Button disabled />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the children', () => {
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders the props.text instead of children', () => {
+    const text      = 'foo';
+    const children  = <p>children</p>;
+    const component = renderer.create(<Button text={text}>{children}</Button>);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('renders class="button--block" if props.block given', () => {
+    const component = renderer.create(<Button block />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('adds class "button-secondary" if props.secondary given', () => {
+    const component = renderer.create(<Button secondary />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.js b/app/javascript/mastodon/components/__tests__/display_name-test.js
new file mode 100644
index 000000000..0d040c4cd
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/display_name-test.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { fromJS }  from 'immutable';
+import DisplayName from '../display_name';
+
+describe('<DisplayName />', () => {
+  it('renders display name + account name', () => {
+    const account = fromJS({
+      username: 'bar',
+      acct: 'bar@baz',
+      display_name_html: '<p>Foo</p>',
+    });
+    const component = renderer.create(<DisplayName account={account} />);
+    const tree      = component.toJSON();
+
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
new file mode 100644
index 000000000..636402172
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -0,0 +1,61 @@
+import emojify from '../emoji';
+
+describe('emoji', () => {
+  describe('.emojify', () => {
+    it('ignores unknown shortcodes', () => {
+      expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
+    });
+
+    it('ignores shortcodes inside of tags', () => {
+      expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
+    });
+
+    it('works with unclosed tags', () => {
+      expect(emojify('hello>')).toEqual('hello>');
+      expect(emojify('<hello')).toEqual('<hello');
+    });
+
+    it('works with unclosed shortcodes', () => {
+      expect(emojify('smile:')).toEqual('smile:');
+      expect(emojify(':smile')).toEqual(':smile');
+    });
+
+    it('does unicode', () => {
+      expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
+        '<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
+      expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual(
+      '<img draggable="false" class="emojione" alt="๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
+      expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
+      expect(emojify('\u2757')).toEqual(
+      '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
+    });
+
+    it('does multiple unicode', () => {
+      expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" />');
+      expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
+        '<img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" />');
+      expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
+        'foo <img draggable="false" class="emojione" alt="โ—" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#๏ธโƒฃ" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
+    });
+
+    it('ignores unicode inside of tags', () => {
+      expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
+    });
+
+    it('does multiple emoji properly (issue 5188)', () => {
+      expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
+      expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ‘Œ" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="๐ŸŒˆ" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="๐Ÿ’•" title=":two_hearts:" src="/emoji/1f495.svg" />');
+    });
+
+    it('does an emoji that has no shortcode', () => {
+      expect(emojify('๐Ÿ•‰๏ธ')).toEqual('<img draggable="false" class="emojione" alt="๐Ÿ•‰๏ธ" title="" src="/emoji/1f549.svg" />');
+    });
+
+    it('does an emoji whose filename is irregular', () => {
+      expect(emojify('โ†™๏ธ')).toEqual('<img draggable="false" class="emojione" alt="โ†™๏ธ" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
+    });
+  });
+});
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
new file mode 100644
index 000000000..53efa5743
--- /dev/null
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji_index-test.js
@@ -0,0 +1,130 @@
+import { pick } from 'lodash';
+import { emojiIndex } from 'emoji-mart';
+import { search } from '../emoji_mart_search_light';
+
+const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
+
+describe('emoji_index', () => {
+  it('should give same result for emoji_index_light and emoji-mart', () => {
+    const expected = [
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '๐Ÿ',
+      },
+    ];
+    expect(search('pineapple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('orders search results correctly', () => {
+    const expected = [
+      {
+        id: 'apple',
+        unified: '1f34e',
+        native: '๐ŸŽ',
+      },
+      {
+        id: 'pineapple',
+        unified: '1f34d',
+        native: '๐Ÿ',
+      },
+      {
+        id: 'green_apple',
+        unified: '1f34f',
+        native: '๐Ÿ',
+      },
+      {
+        id: 'iphone',
+        unified: '1f4f1',
+        native: '๐Ÿ“ฑ',
+      },
+    ];
+    expect(search('apple').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('handles custom emoji', () => {
+    const custom = [
+      {
+        id: 'mastodon',
+        name: 'mastodon',
+        short_names: ['mastodon'],
+        text: '',
+        emoticons: [],
+        keywords: ['mastodon'],
+        imageUrl: 'http://example.com',
+        custom: true,
+      },
+    ];
+    search('', { custom });
+    emojiIndex.search('', { custom });
+    const expected = [
+      {
+        id: 'mastodon',
+        custom: true,
+      },
+    ];
+    expect(search('masto').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('should filter only emojis we care about, exclude pineapple', () => {
+    const emojisToShowFilter = unified => unified !== '1F34D';
+    expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+    expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
+      .not.toContain('pineapple');
+  });
+
+  it('can include/exclude categories', () => {
+    expect(search('flag', { include: ['people'] })).toEqual([]);
+    expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
+  });
+
+  it('does an emoji whose unified name is irregular', () => {
+    const expected = [
+      {
+        'id': 'water_polo',
+        'unified': '1f93d',
+        'native': '๐Ÿคฝ',
+      },
+      {
+        'id': 'man-playing-water-polo',
+        'unified': '1f93d-200d-2642-fe0f',
+        'native': '๐Ÿคฝโ€โ™‚๏ธ',
+      },
+      {
+        'id': 'woman-playing-water-polo',
+        'unified': '1f93d-200d-2640-fe0f',
+        'native': '๐Ÿคฝโ€โ™€๏ธ',
+      },
+    ];
+    expect(search('polo').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for thinking_face', () => {
+    const expected = [
+      {
+        id: 'thinking_face',
+        unified: '1f914',
+        native: '๐Ÿค”',
+      },
+    ];
+    expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
+  });
+
+  it('can search for woman-facepalming', () => {
+    const expected = [
+      {
+        id: 'woman-facepalming',
+        unified: '1f926-200d-2640-fe0f',
+        native: '๐Ÿคฆโ€โ™€๏ธ',
+      },
+    ];
+    expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
+    expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
+  });
+});
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
new file mode 100644
index 000000000..1e5e1d8dc
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import Column from '../column';
+import ColumnHeader from '../column_header';
+
+describe('<Column />', () => {
+  describe('<ColumnHeader /> click handler', () => {
+    const originalRaf = global.requestAnimationFrame;
+
+    beforeEach(() => {
+      global.requestAnimationFrame = jest.fn();
+    });
+
+    afterAll(() => {
+      global.requestAnimationFrame = originalRaf;
+    });
+
+    it('runs the scroll animation if the column contains scrollable content', () => {
+      const wrapper = mount(
+        <Column heading='notifications'>
+          <div className='scrollable' />
+        </Column>
+      );
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
+    });
+
+    it('does not try to scroll if there is no scrollable content', () => {
+      const wrapper = mount(<Column heading='notifications' />);
+      wrapper.find(ColumnHeader).simulate('click');
+      expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
+    });
+  });
+});
diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js
new file mode 100644
index 000000000..80148379b
--- /dev/null
+++ b/app/javascript/mastodon/test_setup.js
@@ -0,0 +1,5 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+const adapter = new Adapter();
+configure({ adapter });