989 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			989 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
/* eslint-disable import/no-commonjs, no-new */
 | 
						|
 | 
						|
import $ from 'jquery';
 | 
						|
import MockAdapter from 'axios-mock-adapter';
 | 
						|
import '~/behaviors/markdown/render_gfm';
 | 
						|
import { createSpyObj } from 'helpers/jest_helpers';
 | 
						|
import { setTestTimeoutOnce } from 'helpers/timeout';
 | 
						|
import { TEST_HOST } from 'helpers/test_constants';
 | 
						|
import * as urlUtility from '~/lib/utils/url_utility';
 | 
						|
import axios from '~/lib/utils/axios_utils';
 | 
						|
 | 
						|
// These must be imported synchronously because they pull dependencies
 | 
						|
// from the DOM.
 | 
						|
window.jQuery = $;
 | 
						|
require('autosize');
 | 
						|
require('~/commons');
 | 
						|
require('~/notes');
 | 
						|
 | 
						|
const { Notes } = window;
 | 
						|
const FLASH_TYPE_ALERT = 'alert';
 | 
						|
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
 | 
						|
const fixture = 'snippets/show.html';
 | 
						|
let mockAxios;
 | 
						|
 | 
						|
window.project_uploads_path = `${TEST_HOST}/uploads`;
 | 
						|
window.gon = window.gon || {};
 | 
						|
window.gl = window.gl || {};
 | 
						|
gl.utils = gl.utils || {};
 | 
						|
gl.utils.disableButtonIfEmptyField = () => {};
 | 
						|
 | 
						|
// the following test is unreliable and failing in master 2-3 times a day
 | 
						|
// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
 | 
						|
// eslint-disable-next-line jest/no-disabled-tests
 | 
						|
describe.skip('Old Notes (~/notes.js)', () => {
 | 
						|
  beforeEach(() => {
 | 
						|
    loadFixtures(fixture);
 | 
						|
 | 
						|
    // Re-declare this here so that test_setup.js#beforeEach() doesn't
 | 
						|
    // overwrite it.
 | 
						|
    mockAxios = new MockAdapter(axios);
 | 
						|
 | 
						|
    $.ajax = () => {
 | 
						|
      throw new Error('$.ajax should not be called through!');
 | 
						|
    };
 | 
						|
 | 
						|
    // These jQuery+DOM tests are super flaky so increase the timeout to avoid
 | 
						|
    // random failures.
 | 
						|
    // It seems that running tests in parallel increases failure rate.
 | 
						|
    jest.setTimeout(4000);
 | 
						|
    setTestTimeoutOnce(4000);
 | 
						|
  });
 | 
						|
 | 
						|
  afterEach(() => {
 | 
						|
    // The Notes component sets a polling interval. Clear it after every run.
 | 
						|
    // Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers().
 | 
						|
    jest.clearAllTimers();
 | 
						|
 | 
						|
    return axios.waitForAll().finally(() => mockAxios.restore());
 | 
						|
  });
 | 
						|
 | 
						|
  it('loads the Notes class into the DOM', () => {
 | 
						|
    expect(Notes).toBeDefined();
 | 
						|
    expect(Notes.name).toBe('Notes');
 | 
						|
  });
 | 
						|
 | 
						|
  describe('addBinding', () => {
 | 
						|
    it('calls postComment when comment button is clicked', () => {
 | 
						|
      jest.spyOn(Notes.prototype, 'postComment');
 | 
						|
 | 
						|
      new window.Notes('', []);
 | 
						|
      $('.js-comment-button').click();
 | 
						|
      expect(Notes.prototype.postComment).toHaveBeenCalled();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('task lists', () => {
 | 
						|
    beforeEach(() => {
 | 
						|
      mockAxios.onAny().reply(200, {});
 | 
						|
      new Notes('', []);
 | 
						|
    });
 | 
						|
 | 
						|
    it('modifies the Markdown field', () => {
 | 
						|
      const changeEvent = document.createEvent('HTMLEvents');
 | 
						|
      changeEvent.initEvent('change', true, true);
 | 
						|
      $('input[type=checkbox]')
 | 
						|
        .attr('checked', true)[0]
 | 
						|
        .dispatchEvent(changeEvent);
 | 
						|
 | 
						|
      expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
 | 
						|
    });
 | 
						|
 | 
						|
    it('submits an ajax request on tasklist:changed', () => {
 | 
						|
      jest.spyOn(axios, 'patch');
 | 
						|
 | 
						|
      const lineNumber = 8;
 | 
						|
      const lineSource = '- [ ] item 8';
 | 
						|
      const index = 3;
 | 
						|
      const checked = true;
 | 
						|
 | 
						|
      $('.js-task-list-container').trigger({
 | 
						|
        type: 'tasklist:changed',
 | 
						|
        detail: { lineNumber, lineSource, index, checked },
 | 
						|
      });
 | 
						|
 | 
						|
      expect(axios.patch).toHaveBeenCalledWith(undefined, {
 | 
						|
        note: {
 | 
						|
          note: '',
 | 
						|
          lock_version: undefined,
 | 
						|
          update_task: { index, checked, line_number: lineNumber, line_source: lineSource },
 | 
						|
        },
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('comments', () => {
 | 
						|
    let notes;
 | 
						|
    let autosizeSpy;
 | 
						|
    let textarea;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
 | 
						|
      textarea = $('.js-note-text');
 | 
						|
      textarea.data('autosave', {
 | 
						|
        reset: () => {},
 | 
						|
      });
 | 
						|
      autosizeSpy = jest.fn();
 | 
						|
      $(textarea).on('autosize:update', autosizeSpy);
 | 
						|
 | 
						|
      jest.spyOn(notes, 'renderNote');
 | 
						|
 | 
						|
      $('.js-comment-button').on('click', e => {
 | 
						|
        const $form = $(this);
 | 
						|
        e.preventDefault();
 | 
						|
        notes.addNote($form, {});
 | 
						|
        notes.reenableTargetFormSubmitButton(e);
 | 
						|
        notes.resetMainTargetForm(e);
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('autosizes after comment submission', () => {
 | 
						|
      textarea.text('This is an example comment note');
 | 
						|
      expect(autosizeSpy).not.toHaveBeenCalled();
 | 
						|
      $('.js-comment-button').click();
 | 
						|
      expect(autosizeSpy).toHaveBeenCalled();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not place escaped text in the comment box in case of error', () => {
 | 
						|
      const deferred = $.Deferred();
 | 
						|
      jest.spyOn($, 'ajax').mockReturnValueOnce(deferred);
 | 
						|
      $(textarea).text('A comment with `markup`.');
 | 
						|
 | 
						|
      deferred.reject();
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      expect($(textarea).val()).toBe('A comment with `markup`.');
 | 
						|
 | 
						|
      $.ajax.mockRestore();
 | 
						|
      expect($.ajax.mock).toBeUndefined();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('updateNote', () => {
 | 
						|
    let notes;
 | 
						|
    let noteEntity;
 | 
						|
    let $notesContainer;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
      window.gon.current_username = 'root';
 | 
						|
      window.gon.current_user_fullname = 'Administrator';
 | 
						|
      const sampleComment = 'foo';
 | 
						|
      noteEntity = {
 | 
						|
        id: 1234,
 | 
						|
        html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
 | 
						|
                <div class="note-text">${sampleComment}</div>
 | 
						|
                </li>`,
 | 
						|
        note: sampleComment,
 | 
						|
        valid: true,
 | 
						|
      };
 | 
						|
 | 
						|
      $notesContainer = $('ul.main-notes-list');
 | 
						|
      const $form = $('form.js-main-target-form');
 | 
						|
      $form.find('textarea.js-note-text').val(sampleComment);
 | 
						|
 | 
						|
      mockAxios.onPost(NOTES_POST_PATH).reply(200, noteEntity);
 | 
						|
    });
 | 
						|
 | 
						|
    it('updates note and resets edit form', () => {
 | 
						|
      jest.spyOn(notes, 'revertNoteEditForm');
 | 
						|
      jest.spyOn(notes, 'setupNewNote');
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
 | 
						|
      const updatedNote = { ...noteEntity };
 | 
						|
      updatedNote.note = 'bar';
 | 
						|
      notes.updateNote(updatedNote, $targetNote);
 | 
						|
 | 
						|
      expect(notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
 | 
						|
      expect(notes.setupNewNote).toHaveBeenCalled();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('updateNoteTargetSelector', () => {
 | 
						|
    const hash = 'note_foo';
 | 
						|
    let $note;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      $note = $(`<div id="${hash}"></div>`);
 | 
						|
      jest.spyOn($note, 'filter');
 | 
						|
      jest.spyOn($note, 'toggleClass');
 | 
						|
    });
 | 
						|
 | 
						|
    // urlUtility is a dependency of the notes module. Its getLocatinHash() method should be called internally.
 | 
						|
 | 
						|
    it('sets target when hash matches', () => {
 | 
						|
      jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
 | 
						|
 | 
						|
      Notes.updateNoteTargetSelector($note);
 | 
						|
 | 
						|
      expect(urlUtility.getLocationHash).toHaveBeenCalled();
 | 
						|
      expect($note.filter).toHaveBeenCalledWith(`#${hash}`);
 | 
						|
      expect($note.toggleClass).toHaveBeenCalledWith('target', true);
 | 
						|
    });
 | 
						|
 | 
						|
    it('unsets target when hash does not match', () => {
 | 
						|
      jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('note_doesnotexist');
 | 
						|
 | 
						|
      Notes.updateNoteTargetSelector($note);
 | 
						|
 | 
						|
      expect(urlUtility.getLocationHash).toHaveBeenCalled();
 | 
						|
      expect($note.toggleClass).toHaveBeenCalledWith('target', false);
 | 
						|
    });
 | 
						|
 | 
						|
    it('unsets target when there is not a hash fragment anymore', () => {
 | 
						|
      jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(null);
 | 
						|
 | 
						|
      Notes.updateNoteTargetSelector($note);
 | 
						|
 | 
						|
      expect(urlUtility.getLocationHash).toHaveBeenCalled();
 | 
						|
      expect($note.toggleClass).toHaveBeenCalledWith('target', false);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('renderNote', () => {
 | 
						|
    let notes;
 | 
						|
    let note;
 | 
						|
    let $notesList;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      note = {
 | 
						|
        id: 1,
 | 
						|
        valid: true,
 | 
						|
        note: 'heya',
 | 
						|
        html: '<div>heya</div>',
 | 
						|
      };
 | 
						|
      $notesList = createSpyObj('$notesList', ['find', 'append']);
 | 
						|
 | 
						|
      notes = createSpyObj('notes', [
 | 
						|
        'setupNewNote',
 | 
						|
        'refresh',
 | 
						|
        'collapseLongCommitList',
 | 
						|
        'updateNotesCount',
 | 
						|
        'putConflictEditWarningInPlace',
 | 
						|
      ]);
 | 
						|
      notes.taskList = createSpyObj('tasklist', ['init']);
 | 
						|
      notes.note_ids = [];
 | 
						|
      notes.updatedNotesTrackingMap = {};
 | 
						|
 | 
						|
      jest.spyOn(Notes, 'isNewNote');
 | 
						|
      jest.spyOn(Notes, 'isUpdatedNote');
 | 
						|
      jest.spyOn(Notes, 'animateAppendNote');
 | 
						|
      jest.spyOn(Notes, 'animateUpdateNote');
 | 
						|
    });
 | 
						|
 | 
						|
    describe('when adding note', () => {
 | 
						|
      it('should call .animateAppendNote', () => {
 | 
						|
        Notes.isNewNote.mockReturnValueOnce(true);
 | 
						|
        Notes.prototype.renderNote.call(notes, note, null, $notesList);
 | 
						|
 | 
						|
        expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('when note was edited', () => {
 | 
						|
      it('should call .animateUpdateNote', () => {
 | 
						|
        Notes.isNewNote.mockReturnValueOnce(false);
 | 
						|
        Notes.isUpdatedNote.mockReturnValueOnce(true);
 | 
						|
        const $note = $('<div>');
 | 
						|
        $notesList.find.mockReturnValueOnce($note);
 | 
						|
        const $newNote = $(note.html);
 | 
						|
        Notes.animateUpdateNote.mockReturnValueOnce($newNote);
 | 
						|
 | 
						|
        Notes.prototype.renderNote.call(notes, note, null, $notesList);
 | 
						|
 | 
						|
        expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
 | 
						|
        expect(notes.setupNewNote).toHaveBeenCalledWith($newNote);
 | 
						|
      });
 | 
						|
 | 
						|
      describe('while editing', () => {
 | 
						|
        it('should update textarea if nothing has been touched', () => {
 | 
						|
          Notes.isNewNote.mockReturnValueOnce(false);
 | 
						|
          Notes.isUpdatedNote.mockReturnValueOnce(true);
 | 
						|
          const $note = $(`<div class="is-editing">
 | 
						|
            <div class="original-note-content">initial</div>
 | 
						|
            <textarea class="js-note-text">initial</textarea>
 | 
						|
          </div>`);
 | 
						|
          $notesList.find.mockReturnValueOnce($note);
 | 
						|
          Notes.prototype.renderNote.call(notes, note, null, $notesList);
 | 
						|
 | 
						|
          expect($note.find('.js-note-text').val()).toEqual(note.note);
 | 
						|
        });
 | 
						|
 | 
						|
        it('should call .putConflictEditWarningInPlace', () => {
 | 
						|
          Notes.isNewNote.mockReturnValueOnce(false);
 | 
						|
          Notes.isUpdatedNote.mockReturnValueOnce(true);
 | 
						|
          const $note = $(`<div class="is-editing">
 | 
						|
            <div class="original-note-content">initial</div>
 | 
						|
            <textarea class="js-note-text">different</textarea>
 | 
						|
          </div>`);
 | 
						|
          $notesList.find.mockReturnValueOnce($note);
 | 
						|
          Notes.prototype.renderNote.call(notes, note, null, $notesList);
 | 
						|
 | 
						|
          expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
 | 
						|
        });
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('isUpdatedNote', () => {
 | 
						|
    it('should consider same note text as the same', () => {
 | 
						|
      const result = Notes.isUpdatedNote(
 | 
						|
        {
 | 
						|
          note: 'initial',
 | 
						|
        },
 | 
						|
        $(`<div>
 | 
						|
          <div class="original-note-content">initial</div>
 | 
						|
        </div>`),
 | 
						|
      );
 | 
						|
 | 
						|
      expect(result).toEqual(false);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should consider same note with trailing newline as the same', () => {
 | 
						|
      const result = Notes.isUpdatedNote(
 | 
						|
        {
 | 
						|
          note: 'initial\n',
 | 
						|
        },
 | 
						|
        $(`<div>
 | 
						|
          <div class="original-note-content">initial\n</div>
 | 
						|
        </div>`),
 | 
						|
      );
 | 
						|
 | 
						|
      expect(result).toEqual(false);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should consider different notes as different', () => {
 | 
						|
      const result = Notes.isUpdatedNote(
 | 
						|
        {
 | 
						|
          note: 'foo',
 | 
						|
        },
 | 
						|
        $(`<div>
 | 
						|
          <div class="original-note-content">bar</div>
 | 
						|
        </div>`),
 | 
						|
      );
 | 
						|
 | 
						|
      expect(result).toEqual(true);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('renderDiscussionNote', () => {
 | 
						|
    let discussionContainer;
 | 
						|
    let note;
 | 
						|
    let notes;
 | 
						|
    let $form;
 | 
						|
    let row;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      note = {
 | 
						|
        html: '<li></li>',
 | 
						|
        discussion_html: '<div></div>',
 | 
						|
        discussion_id: 1,
 | 
						|
        discussion_resolvable: false,
 | 
						|
        diff_discussion_html: false,
 | 
						|
      };
 | 
						|
      $form = createSpyObj('$form', ['closest', 'find']);
 | 
						|
      $form.length = 1;
 | 
						|
      row = createSpyObj('row', ['prevAll', 'first', 'find']);
 | 
						|
 | 
						|
      notes = createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
 | 
						|
      notes.note_ids = [];
 | 
						|
 | 
						|
      jest.spyOn(Notes, 'isNewNote');
 | 
						|
      jest.spyOn(Notes, 'animateAppendNote').mockImplementation();
 | 
						|
      Notes.isNewNote.mockReturnValue(true);
 | 
						|
      notes.isParallelView.mockReturnValue(false);
 | 
						|
      row.prevAll.mockReturnValue(row);
 | 
						|
      row.first.mockReturnValue(row);
 | 
						|
      row.find.mockReturnValue(row);
 | 
						|
    });
 | 
						|
 | 
						|
    describe('Discussion root note', () => {
 | 
						|
      let body;
 | 
						|
 | 
						|
      beforeEach(() => {
 | 
						|
        body = createSpyObj('body', ['attr']);
 | 
						|
        discussionContainer = { length: 0 };
 | 
						|
 | 
						|
        $form.closest.mockReturnValueOnce(row).mockReturnValue($form);
 | 
						|
        $form.find.mockReturnValue(discussionContainer);
 | 
						|
        body.attr.mockReturnValue('');
 | 
						|
      });
 | 
						|
 | 
						|
      it('should call Notes.animateAppendNote', () => {
 | 
						|
        Notes.prototype.renderDiscussionNote.call(notes, note, $form);
 | 
						|
 | 
						|
        expect(Notes.animateAppendNote).toHaveBeenCalledWith(
 | 
						|
          note.discussion_html,
 | 
						|
          $('.main-notes-list'),
 | 
						|
        );
 | 
						|
      });
 | 
						|
 | 
						|
      it('should append to row selected with line_code', () => {
 | 
						|
        $form.length = 0;
 | 
						|
        note.discussion_line_code = 'line_code';
 | 
						|
        note.diff_discussion_html = '<tr></tr>';
 | 
						|
 | 
						|
        const line = document.createElement('div');
 | 
						|
        line.id = note.discussion_line_code;
 | 
						|
        document.body.appendChild(line);
 | 
						|
 | 
						|
        // Override mocks for this single test
 | 
						|
        $form.closest.mockReset();
 | 
						|
        $form.closest.mockReturnValue($form);
 | 
						|
 | 
						|
        Notes.prototype.renderDiscussionNote.call(notes, note, $form);
 | 
						|
 | 
						|
        expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('Discussion sub note', () => {
 | 
						|
      beforeEach(() => {
 | 
						|
        discussionContainer = { length: 1 };
 | 
						|
 | 
						|
        $form.closest.mockReturnValueOnce(row).mockReturnValueOnce($form);
 | 
						|
        $form.find.mockReturnValue(discussionContainer);
 | 
						|
 | 
						|
        Notes.prototype.renderDiscussionNote.call(notes, note, $form);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should call Notes.animateAppendNote', () => {
 | 
						|
        expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('animateAppendNote', () => {
 | 
						|
    let noteHTML;
 | 
						|
    let $notesList;
 | 
						|
    let $resultantNote;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      noteHTML = '<div></div>';
 | 
						|
      $notesList = createSpyObj('$notesList', ['append']);
 | 
						|
 | 
						|
      $resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should have `fade-in-full` class', () => {
 | 
						|
      expect($resultantNote.hasClass('fade-in-full')).toEqual(true);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should append note to the notes list', () => {
 | 
						|
      expect($notesList.append).toHaveBeenCalledWith($resultantNote);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('animateUpdateNote', () => {
 | 
						|
    let noteHTML;
 | 
						|
    let $note;
 | 
						|
    let $updatedNote;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      noteHTML = '<div></div>';
 | 
						|
      $note = createSpyObj('$note', ['replaceWith']);
 | 
						|
 | 
						|
      $updatedNote = Notes.animateUpdateNote(noteHTML, $note);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should have `fade-in` class', () => {
 | 
						|
      expect($updatedNote.hasClass('fade-in')).toEqual(true);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call replaceWith on $note', () => {
 | 
						|
      expect($note.replaceWith).toHaveBeenCalledWith($updatedNote);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('putEditFormInPlace', () => {
 | 
						|
    it('should call GLForm with GFM parameter passed through', () => {
 | 
						|
      const notes = new Notes('', []);
 | 
						|
      const $el = $(`
 | 
						|
        <div>
 | 
						|
          <form></form>
 | 
						|
        </div>
 | 
						|
      `);
 | 
						|
 | 
						|
      notes.putEditFormInPlace($el);
 | 
						|
 | 
						|
      expect(notes.glForm.enableGFM).toBeTruthy();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('postComment & updateComment', () => {
 | 
						|
    const sampleComment = 'foo';
 | 
						|
    const note = {
 | 
						|
      id: 1234,
 | 
						|
      html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
 | 
						|
              <div class="note-text">${sampleComment}</div>
 | 
						|
              </li>`,
 | 
						|
      note: sampleComment,
 | 
						|
      valid: true,
 | 
						|
    };
 | 
						|
    let notes;
 | 
						|
    let $form;
 | 
						|
    let $notesContainer;
 | 
						|
 | 
						|
    function mockNotesPost() {
 | 
						|
      mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
 | 
						|
    }
 | 
						|
 | 
						|
    function mockNotesPostError() {
 | 
						|
      mockAxios.onPost(NOTES_POST_PATH).networkError();
 | 
						|
    }
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
      window.gon.current_username = 'root';
 | 
						|
      window.gon.current_user_fullname = 'Administrator';
 | 
						|
      $form = $('form.js-main-target-form');
 | 
						|
      $notesContainer = $('ul.main-notes-list');
 | 
						|
      $form.find('textarea.js-note-text').val(sampleComment);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should show placeholder note while new comment is being posted', () => {
 | 
						|
      mockNotesPost();
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      expect($notesContainer.find('.note.being-posted').length).toBeGreaterThan(0);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should remove placeholder note when new comment is done posting', done => {
 | 
						|
      mockNotesPost();
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        expect($notesContainer.find('.note.being-posted').length).toEqual(0);
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('postComment', () => {
 | 
						|
      it('disables the submit button', done => {
 | 
						|
        const $submitButton = $form.find('.js-comment-submit-button');
 | 
						|
 | 
						|
        expect($submitButton).not.toBeDisabled();
 | 
						|
        const dummyEvent = {
 | 
						|
          preventDefault() {},
 | 
						|
          target: $submitButton,
 | 
						|
        };
 | 
						|
        mockAxios.onPost(NOTES_POST_PATH).replyOnce(() => {
 | 
						|
          expect($submitButton).toBeDisabled();
 | 
						|
          return [200, note];
 | 
						|
        });
 | 
						|
 | 
						|
        notes
 | 
						|
          .postComment(dummyEvent)
 | 
						|
          .then(() => {
 | 
						|
            expect($submitButton).not.toBeDisabled();
 | 
						|
          })
 | 
						|
          .then(done)
 | 
						|
          .catch(done.fail);
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('should show actual note element when new comment is done posting', done => {
 | 
						|
      mockNotesPost();
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0);
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('should reset Form when new comment is done posting', done => {
 | 
						|
      mockNotesPost();
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        expect($form.find('textarea.js-note-text').val()).toEqual('');
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('should show flash error message when new comment failed to be posted', done => {
 | 
						|
      mockNotesPostError();
 | 
						|
      jest.spyOn(notes, 'addFlash');
 | 
						|
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        expect(notes.addFlash).toHaveBeenCalled();
 | 
						|
        // JSDom doesn't support the :visible selector yet
 | 
						|
        expect(notes.flashContainer.style.display).not.toBe('none');
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('postComment with Slash commands', () => {
 | 
						|
    const sampleComment = '/assign @root\n/award :100:';
 | 
						|
    const note = {
 | 
						|
      commands_changes: {
 | 
						|
        assignee_id: 1,
 | 
						|
        emoji_award: '100',
 | 
						|
      },
 | 
						|
      errors: {
 | 
						|
        commands_only: ['Commands applied'],
 | 
						|
      },
 | 
						|
      valid: false,
 | 
						|
    };
 | 
						|
    let $form;
 | 
						|
    let $notesContainer;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
 | 
						|
 | 
						|
      new Notes('', []);
 | 
						|
      window.gon.current_username = 'root';
 | 
						|
      window.gon.current_user_fullname = 'Administrator';
 | 
						|
      gl.awardsHandler = {
 | 
						|
        addAwardToEmojiBar: () => {},
 | 
						|
        scrollToAwards: () => {},
 | 
						|
      };
 | 
						|
      gl.GfmAutoComplete = {
 | 
						|
        dataSources: {
 | 
						|
          commands: '/root/test-project/autocomplete_sources/commands',
 | 
						|
        },
 | 
						|
      };
 | 
						|
      $form = $('form.js-main-target-form');
 | 
						|
      $notesContainer = $('ul.main-notes-list');
 | 
						|
      $form.find('textarea.js-note-text').val(sampleComment);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should remove slash command placeholder when comment with slash commands is done posting', done => {
 | 
						|
      jest.spyOn(gl.awardsHandler, 'addAwardToEmojiBar');
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('update comment with script tags', () => {
 | 
						|
    const sampleComment = '<script></script>';
 | 
						|
    const updatedComment = '<script></script>';
 | 
						|
    const note = {
 | 
						|
      id: 1234,
 | 
						|
      html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
 | 
						|
              <div class="note-text">${sampleComment}</div>
 | 
						|
              </li>`,
 | 
						|
      note: sampleComment,
 | 
						|
      valid: true,
 | 
						|
    };
 | 
						|
    let $form;
 | 
						|
    let $notesContainer;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
 | 
						|
 | 
						|
      new Notes('', []);
 | 
						|
      window.gon.current_username = 'root';
 | 
						|
      window.gon.current_user_fullname = 'Administrator';
 | 
						|
      $form = $('form.js-main-target-form');
 | 
						|
      $notesContainer = $('ul.main-notes-list');
 | 
						|
      $form.find('textarea.js-note-text').html(sampleComment);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not render a script tag', done => {
 | 
						|
      $('.js-comment-button').click();
 | 
						|
 | 
						|
      setImmediate(() => {
 | 
						|
        const $noteEl = $notesContainer.find(`#note_${note.id}`);
 | 
						|
        $noteEl.find('.js-note-edit').click();
 | 
						|
        $noteEl.find('textarea.js-note-text').html(updatedComment);
 | 
						|
        $noteEl.find('.js-comment-save-button').click();
 | 
						|
 | 
						|
        const $updatedNoteEl = $notesContainer
 | 
						|
          .find(`#note_${note.id}`)
 | 
						|
          .find('.js-task-list-container');
 | 
						|
 | 
						|
        expect(
 | 
						|
          $updatedNoteEl
 | 
						|
            .find('.note-text')
 | 
						|
            .text()
 | 
						|
            .trim(),
 | 
						|
        ).toEqual('');
 | 
						|
 | 
						|
        done();
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('getFormData', () => {
 | 
						|
    let $form;
 | 
						|
    let sampleComment;
 | 
						|
    let notes;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
 | 
						|
      $form = $('form');
 | 
						|
      sampleComment = 'foobar';
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return form metadata object from form reference', () => {
 | 
						|
      $form.find('textarea.js-note-text').val(sampleComment);
 | 
						|
      const { formData, formContent, formAction } = notes.getFormData($form);
 | 
						|
 | 
						|
      expect(formData.indexOf(sampleComment)).toBeGreaterThan(-1);
 | 
						|
      expect(formContent).toEqual(sampleComment);
 | 
						|
      expect(formAction).toEqual($form.attr('action'));
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return form metadata with sanitized formContent from form reference', () => {
 | 
						|
      sampleComment = '<script>alert("Boom!");</script>';
 | 
						|
      $form.find('textarea.js-note-text').val(sampleComment);
 | 
						|
 | 
						|
      const { formContent } = notes.getFormData($form);
 | 
						|
 | 
						|
      expect(formContent).toEqual('<script>alert("Boom!");</script>');
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('hasQuickActions', () => {
 | 
						|
    let notes;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return true when comment begins with a quick action', () => {
 | 
						|
      const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
 | 
						|
      const hasQuickActions = notes.hasQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(hasQuickActions).toBeTruthy();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return false when comment does NOT begin with a quick action', () => {
 | 
						|
      const sampleComment = 'Hey, /unassign Merging this';
 | 
						|
      const hasQuickActions = notes.hasQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(hasQuickActions).toBeFalsy();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return false when comment does NOT have any quick actions', () => {
 | 
						|
      const sampleComment = 'Looking good, Awesome!';
 | 
						|
      const hasQuickActions = notes.hasQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(hasQuickActions).toBeFalsy();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('stripQuickActions', () => {
 | 
						|
    it('should strip quick actions from the comment which begins with a quick action', () => {
 | 
						|
      const notes = new Notes();
 | 
						|
      const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
 | 
						|
      const stripedComment = notes.stripQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(stripedComment).toBe('');
 | 
						|
    });
 | 
						|
 | 
						|
    it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
 | 
						|
      const notes = new Notes();
 | 
						|
      const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
 | 
						|
      const stripedComment = notes.stripQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(stripedComment).toBe('Merging this');
 | 
						|
    });
 | 
						|
 | 
						|
    it('should NOT strip string that has slashes within', () => {
 | 
						|
      const notes = new Notes();
 | 
						|
      const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
 | 
						|
      const stripedComment = notes.stripQuickActions(sampleComment);
 | 
						|
 | 
						|
      expect(stripedComment).toBe(sampleComment);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('getQuickActionDescription', () => {
 | 
						|
    const availableQuickActions = [
 | 
						|
      { name: 'close', description: 'Close this issue', params: [] },
 | 
						|
      { name: 'title', description: 'Change title', params: [{}] },
 | 
						|
      { name: 'estimate', description: 'Set time estimate', params: [{}] },
 | 
						|
    ];
 | 
						|
    let notes;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return executing quick action description when note has single quick action', () => {
 | 
						|
      const sampleComment = '/close';
 | 
						|
 | 
						|
      expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
 | 
						|
        'Applying command to close this issue',
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return generic multiple quick action description when note has multiple quick actions', () => {
 | 
						|
      const sampleComment = '/close\n/title [Duplicate] Issue foobar';
 | 
						|
 | 
						|
      expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
 | 
						|
        'Applying multiple commands',
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return generic quick action description when available quick actions list is not populated', () => {
 | 
						|
      const sampleComment = '/close\n/title [Duplicate] Issue foobar';
 | 
						|
 | 
						|
      expect(notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('createPlaceholderNote', () => {
 | 
						|
    const sampleComment = 'foobar';
 | 
						|
    const uniqueId = 'b1234-a4567';
 | 
						|
    const currentUsername = 'root';
 | 
						|
    const currentUserFullname = 'Administrator';
 | 
						|
    const currentUserAvatar = 'avatar_url';
 | 
						|
    let notes;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return constructed placeholder element for regular note based on form contents', () => {
 | 
						|
      const $tempNote = notes.createPlaceholderNote({
 | 
						|
        formContent: sampleComment,
 | 
						|
        uniqueId,
 | 
						|
        isDiscussionNote: false,
 | 
						|
        currentUsername,
 | 
						|
        currentUserFullname,
 | 
						|
        currentUserAvatar,
 | 
						|
      });
 | 
						|
      const $tempNoteHeader = $tempNote.find('.note-header');
 | 
						|
 | 
						|
      expect($tempNote.prop('nodeName')).toEqual('LI');
 | 
						|
      expect($tempNote.attr('id')).toEqual(uniqueId);
 | 
						|
      expect($tempNote.hasClass('being-posted')).toBeTruthy();
 | 
						|
      expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
 | 
						|
      $tempNote.find('.timeline-icon > a, .note-header-info > a').each((i, el) => {
 | 
						|
        expect(el.getAttribute('href')).toEqual(`/${currentUsername}`);
 | 
						|
      });
 | 
						|
 | 
						|
      expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
 | 
						|
      expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
 | 
						|
      expect(
 | 
						|
        $tempNoteHeader
 | 
						|
          .find('.d-none.d-sm-inline-block')
 | 
						|
          .text()
 | 
						|
          .trim(),
 | 
						|
      ).toEqual(currentUserFullname);
 | 
						|
 | 
						|
      expect(
 | 
						|
        $tempNoteHeader
 | 
						|
          .find('.note-headline-light')
 | 
						|
          .text()
 | 
						|
          .trim(),
 | 
						|
      ).toEqual(`@${currentUsername}`);
 | 
						|
 | 
						|
      expect(
 | 
						|
        $tempNote
 | 
						|
          .find('.note-body .note-text p')
 | 
						|
          .text()
 | 
						|
          .trim(),
 | 
						|
      ).toEqual(sampleComment);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return constructed placeholder element for discussion note based on form contents', () => {
 | 
						|
      const $tempNote = notes.createPlaceholderNote({
 | 
						|
        formContent: sampleComment,
 | 
						|
        uniqueId,
 | 
						|
        isDiscussionNote: true,
 | 
						|
        currentUsername,
 | 
						|
        currentUserFullname,
 | 
						|
      });
 | 
						|
 | 
						|
      expect($tempNote.prop('nodeName')).toEqual('LI');
 | 
						|
      expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return a escaped user name', () => {
 | 
						|
      const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>';
 | 
						|
      const $tempNote = notes.createPlaceholderNote({
 | 
						|
        formContent: sampleComment,
 | 
						|
        uniqueId,
 | 
						|
        isDiscussionNote: false,
 | 
						|
        currentUsername,
 | 
						|
        currentUserFullname: currentUserFullnameXSS,
 | 
						|
        currentUserAvatar,
 | 
						|
      });
 | 
						|
      const $tempNoteHeader = $tempNote.find('.note-header');
 | 
						|
 | 
						|
      expect(
 | 
						|
        $tempNoteHeader
 | 
						|
          .find('.d-none.d-sm-inline-block')
 | 
						|
          .text()
 | 
						|
          .trim(),
 | 
						|
      ).toEqual('Foo <script>alert("XSS")</script>');
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('createPlaceholderSystemNote', () => {
 | 
						|
    const sampleCommandDescription = 'Applying command to close this issue';
 | 
						|
    const uniqueId = 'b1234-a4567';
 | 
						|
    let notes;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      notes = new Notes('', []);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return constructed placeholder element for system note based on form contents', () => {
 | 
						|
      const $tempNote = notes.createPlaceholderSystemNote({
 | 
						|
        formContent: sampleCommandDescription,
 | 
						|
        uniqueId,
 | 
						|
      });
 | 
						|
 | 
						|
      expect($tempNote.prop('nodeName')).toEqual('LI');
 | 
						|
      expect($tempNote.attr('id')).toEqual(uniqueId);
 | 
						|
      expect($tempNote.hasClass('being-posted')).toBeTruthy();
 | 
						|
      expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
 | 
						|
      expect(
 | 
						|
        $tempNote
 | 
						|
          .find('.timeline-content i')
 | 
						|
          .text()
 | 
						|
          .trim(),
 | 
						|
      ).toEqual(sampleCommandDescription);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('appendFlash', () => {
 | 
						|
    it('shows a flash message', () => {
 | 
						|
      const notes = new Notes('', []);
 | 
						|
      notes.addFlash('Error message', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
 | 
						|
 | 
						|
      const flash = $('.flash-alert')[0];
 | 
						|
      expect(document.contains(flash)).toBe(true);
 | 
						|
      expect(flash.parentNode.style.display).toBe('block');
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('clearFlash', () => {
 | 
						|
    beforeEach(() => {
 | 
						|
      $(document).off('ajax:success');
 | 
						|
    });
 | 
						|
 | 
						|
    it('hides visible flash message', () => {
 | 
						|
      const notes = new Notes('', []);
 | 
						|
      notes.addFlash('Error message 1', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
 | 
						|
      const flash = $('.flash-alert')[0];
 | 
						|
      notes.clearFlash();
 | 
						|
      expect(flash.parentNode.style.display).toBe('none');
 | 
						|
      expect(notes.flashContainer).toBeNull();
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |