gl_dropdown_spec.js 8.7 KB
Newer Older
1
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
2

3 4
import '~/gl_dropdown';
import '~/lib/utils/common_utils';
5
import * as urlUtils from '~/lib/utils/url_utility';
6

7 8 9 10
describe('glDropdown', function describeDropdown() {
  preloadFixtures('static/gl_dropdown.html.raw');
  loadJSONFixtures('projects.json');

11
  const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
12
  const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
13 14
  const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
  const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
15

16 17 18 19 20 21
  const ARROW_KEYS = {
    DOWN: 40,
    UP: 38,
    ENTER: 13,
    ESC: 27
  };
22

23 24
  let remoteCallback;

25
  const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
26
    i = i || 0;
27
    if (!i) direction = direction.toUpperCase();
28 29
    $('body').trigger({
      type: 'keydown',
30 31
      which: ARROW_KEYS[direction],
      keyCode: ARROW_KEYS[direction]
32
    });
33
    i += 1;
34 35 36 37
    if (i <= steps) {
      navigateWithKeys(direction, steps, cb, i);
    } else {
      cb();
38
    }
39
  };
40

41
  const remoteMock = function remoteMock(data, term, callback) {
42
    remoteCallback = callback.bind({}, data);
43
  };
44

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
    const options = Object.assign({
      selectable: true,
      filterable: isFilterable,
      data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
      search: {
        fields: ['name']
      },
      text: project => (project.name_with_namespace || project.name),
      id: project => project.id,
    }, extraOpts);
    this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
  }

  beforeEach(() => {
    loadFixtures('static/gl_dropdown.html.raw');
    this.dropdownContainerElement = $('.dropdown.inline');
    this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
    this.projectsData = getJSONFixture('projects.json');
  });
65

66
  afterEach(() => {
Phil Hughes's avatar
Phil Hughes committed
67 68
    $('body').off('keydown');
    this.dropdownContainerElement.off('keyup');
69
  });
70

71 72 73 74 75 76
  it('should open on click', () => {
    initDropDown.call(this, false);
    expect(this.dropdownContainerElement).not.toHaveClass('open');
    this.dropdownButtonElement.click();
    expect(this.dropdownContainerElement).toHaveClass('open');
  });
77

78 79
  it('escapes HTML as text', () => {
    this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
80

81
    initDropDown.call(this, false);
82

83
    this.dropdownButtonElement.click();
84

85 86 87 88
    expect(
      $('.dropdown-content li:first-child').text(),
    ).toBe('<script>alert("testing");</script>');
  });
89

90 91 92
  it('should output HTML when highlighting', () => {
    this.projectsData[0].name_with_namespace = 'testing';
    $('.dropdown-input .dropdown-input-field').val('test');
93

94 95 96
    initDropDown.call(this, false, true, {
      highlight: true,
    });
97

98
    this.dropdownButtonElement.click();
99

100 101 102
    expect(
      $('.dropdown-content li:first-child').text(),
    ).toBe('testing');
103

104 105 106 107
    expect(
      $('.dropdown-content li:first-child a').html(),
    ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
  });
108

109 110 111 112
  describe('that is open', () => {
    beforeEach(() => {
      initDropDown.call(this, false, false);
      this.dropdownButtonElement.click();
113 114
    });

115 116 117 118 119 120
    it('should select a following item on DOWN keypress', () => {
      expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
      const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
      navigateWithKeys('down', randomIndex, () => {
        expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
        expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
121
      });
122
    });
123

124 125 126 127 128 129
    it('should select a previous item on UP keypress', () => {
      expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
      navigateWithKeys('down', (this.projectsData.length - 1), () => {
        expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
        const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
        navigateWithKeys('up', randomIndex, () => {
130
          expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
131
          expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
132
        });
133
      });
134
    });
135

136 137 138 139
    it('should click the selected item on ENTER keypress', () => {
      expect(this.dropdownContainerElement).toHaveClass('open');
      const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
      navigateWithKeys('down', randomIndex, () => {
140
        spyOn(urlUtils, 'visitUrl').and.stub();
141 142 143 144 145
        navigateWithKeys('enter', null, () => {
          expect(this.dropdownContainerElement).not.toHaveClass('open');
          const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
          expect(link).toHaveClass('is-active');
          const linkedLocation = link.attr('href');
146
          if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
147 148
        });
      });
149
    });
150

151 152 153 154 155 156
    it('should close on ESC keypress', () => {
      expect(this.dropdownContainerElement).toHaveClass('open');
      this.dropdownContainerElement.trigger({
        type: 'keyup',
        which: ARROW_KEYS.ESC,
        keyCode: ARROW_KEYS.ESC
157
      });
158 159 160
      expect(this.dropdownContainerElement).not.toHaveClass('open');
    });
  });
161

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  describe('opened and waiting for a remote callback', () => {
    beforeEach(() => {
      initDropDown.call(this, true, true);
      this.dropdownButtonElement.click();
    });

    it('should show loading indicator while search results are being fetched by backend', () => {
      const dropdownMenu = document.querySelector('.dropdown-menu');

      expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
      remoteCallback();
      expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
    });

    it('should not focus search input while remote task is not complete', () => {
      expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
      remoteCallback();
      expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
    });

    it('should focus search input after remote task is complete', () => {
      remoteCallback();
      expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
    });

    it('should focus on input when opening for the second time after transition', () => {
      remoteCallback();
      this.dropdownContainerElement.trigger({
        type: 'keyup',
        which: ARROW_KEYS.ESC,
        keyCode: ARROW_KEYS.ESC
193
      });
194 195 196
      this.dropdownButtonElement.click();
      this.dropdownContainerElement.trigger('transitionend');
      expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
197
    });
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
  });

  describe('input focus with array data', () => {
    it('should focus input when passing array data to drop down', () => {
      initDropDown.call(this, false, true);
      this.dropdownButtonElement.click();
      this.dropdownContainerElement.trigger('transitionend');
      expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
    });
  });

  it('should still have input value on close and restore', () => {
    const $searchInput = $(SEARCH_INPUT_SELECTOR);
    initDropDown.call(this, false, true);
    $searchInput
      .trigger('focus')
      .val('g')
      .trigger('input');
    expect($searchInput.val()).toEqual('g');
    this.dropdownButtonElement.trigger('hidden.bs.dropdown');
    $searchInput
      .trigger('blur')
      .trigger('focus');
    expect($searchInput.val()).toEqual('g');
  });

  describe('renderItem', () => {
    describe('without selected value', () => {
      let dropdown;
227 228

      beforeEach(() => {
229 230 231 232 233 234
        const dropdownOptions = {

        };
        const $dropdownDiv = $('<div />');
        $dropdownDiv.glDropdown(dropdownOptions);
        dropdown = $dropdownDiv.data('glDropdown');
235 236
      });

237 238
      it('marks items without ID as active', () => {
        const dummyData = { };
Clement Ho's avatar
Clement Ho committed
239

240
        const html = dropdown.renderItem(dummyData, null, null);
Clement Ho's avatar
Clement Ho committed
241

242 243
        const link = html.querySelector('a');
        expect(link).toHaveClass('is-active');
244 245
      });

246 247 248 249
      it('does not mark items with ID as active', () => {
        const dummyData = {
          id: 'ea'
        };
250

251
        const html = dropdown.renderItem(dummyData, null, null);
252

253 254
        const link = html.querySelector('a');
        expect(link).not.toHaveClass('is-active');
255 256
      });
    });
257
  });
258
});