From f21a31559031c283cbc9cf99a11e98fa519ca0e5 Mon Sep 17 00:00:00 2001
From: Eric Eastwood <contact@ericeastwood.com>
Date: Thu, 18 May 2017 02:46:44 -0500
Subject: [PATCH] Fix missing .original-note-content and trailing new line edge
 case

Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32449
---
 app/assets/javascripts/notes.js           |  42 ++++-----
 app/views/shared/notes/_edit.html.haml    |   2 -
 app/views/shared/notes/_note.html.haml    |   2 +
 spec/features/issues/note_polling_spec.rb | 101 +++++++++++++++-------
 spec/javascripts/notes_spec.js            |  60 +++++++++++--
 5 files changed, 144 insertions(+), 63 deletions(-)

diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 91d1afba7b..8122e88a15 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -306,7 +306,7 @@ const normalizeNewlines = function(str) {
       }
 
       const $note = $notesList.find(`#note_${noteEntity.id}`);
-      if (this.isNewNote(noteEntity)) {
+      if (Notes.isNewNote(noteEntity, this.note_ids)) {
         this.note_ids.push(noteEntity.id);
 
         const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
@@ -319,7 +319,7 @@ const normalizeNewlines = function(str) {
         return this.updateNotesCount(1);
       }
       // The server can send the same update multiple times so we need to make sure to only update once per actual update.
-      else if (this.isUpdatedNote(noteEntity, $note)) {
+      else if (Notes.isUpdatedNote(noteEntity, $note)) {
         const isEditing = $note.hasClass('is-editing');
         const initialContent = normalizeNewlines(
           $note.find('.original-note-content').text().trim()
@@ -347,23 +347,6 @@ const normalizeNewlines = function(str) {
       }
     };
 
-    /*
-    Check if note does not exists on page
-     */
-
-    Notes.prototype.isNewNote = function(noteEntity) {
-      return $.inArray(noteEntity.id, this.note_ids) === -1;
-    };
-
-    Notes.prototype.isUpdatedNote = function(noteEntity, $note) {
-      // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
-      const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
-      const currentNoteText = normalizeNewlines(
-        $note.find('.original-note-content').text().trim()
-      );
-      return sanitizedNoteNote !== currentNoteText;
-    };
-
     Notes.prototype.isParallelView = function() {
       return Cookies.get('diff_view') === 'parallel';
     };
@@ -376,7 +359,7 @@ const normalizeNewlines = function(str) {
 
     Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
       var discussionContainer, form, row, lineType, diffAvatarContainer;
-      if (!this.isNewNote(noteEntity)) {
+      if (!Notes.isNewNote(noteEntity, this.note_ids)) {
         return;
       }
       this.note_ids.push(noteEntity.id);
@@ -1137,6 +1120,25 @@ const normalizeNewlines = function(str) {
       return $form;
     };
 
+    /**
+     * Check if note does not exists on page
+     */
+    Notes.isNewNote = function(noteEntity, noteIds) {
+      return $.inArray(noteEntity.id, noteIds) === -1;
+    };
+
+    /**
+     * Check if $note already contains the `noteEntity` content
+     */
+    Notes.isUpdatedNote = function(noteEntity, $note) {
+      // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
+      const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
+      const currentNoteText = normalizeNewlines(
+        $note.find('.original-note-content').text().trim()
+      );
+      return sanitizedNoteEntityText !== currentNoteText;
+    };
+
     Notes.checkMergeRequestStatus = function() {
       if (gl.utils.getPagePath(1) === 'merge_requests') {
         gl.mrWidget.checkStatus();
diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml
index 4a02086582..f4b3aac29b 100644
--- a/app/views/shared/notes/_edit.html.haml
+++ b/app/views/shared/notes/_edit.html.haml
@@ -1,3 +1 @@
-.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
-  #{note.note}
 %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 87aae79396..53d0e837aa 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -43,6 +43,8 @@
         .note-text.md
           = note.redacted_note_html
         = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
+        .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+          #{note.note}
         - if note_editable
           = render 'shared/notes/edit', note: note
         .note-awards
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 58b3215f14..da81fa4e36 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -18,58 +18,93 @@ feature 'Issue notes polling', :feature, :js do
   end
 
   describe 'updates' do
-    let(:user) { create(:user) }
-    let(:note_text) { "Hello World" }
-    let(:updated_text) { "Bye World" }
-    let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
+    context 'when from own user' do
+      let(:user) { create(:user) }
+      let(:note_text) { "Hello World" }
+      let(:updated_text) { "Bye World" }
+      let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) }
 
-    before do
-      login_as(user)
-      visit namespace_project_issue_path(project.namespace, project, issue)
-    end
+      before do
+        login_as(user)
+        visit namespace_project_issue_path(project.namespace, project, issue)
+      end
 
-    it 'displays the updated content' do
-      expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+      it 'has .original-note-content to compare against' do
+        expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+        expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", visible: false)
 
-      update_note(existing_note, updated_text)
+        update_note(existing_note, updated_text)
 
-      expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
-    end
+        expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+        expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", visible: false)
+      end
 
-    it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
-      find("#note_#{existing_note.id} .js-note-edit").click
+      it 'displays the updated content' do
+        expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
 
-      expect(page).to have_field("note[note]", with: note_text)
+        update_note(existing_note, updated_text)
 
-      update_note(existing_note, updated_text)
+        expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+      end
 
-      expect(page).to have_field("note[note]", with: updated_text)
-    end
+      it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
+        find("#note_#{existing_note.id} .js-note-edit").click
 
-    it 'when editing but you changed some things, and an update comes in, show a warning' do
-      find("#note_#{existing_note.id} .js-note-edit").click
+        expect(page).to have_field("note[note]", with: note_text)
 
-      expect(page).to have_field("note[note]", with: note_text)
+        update_note(existing_note, updated_text)
 
-      find("#note_#{existing_note.id} .js-note-text").set('something random')
+        expect(page).to have_field("note[note]", with: updated_text)
+      end
 
-      update_note(existing_note, updated_text)
+      it 'when editing but you changed some things, and an update comes in, show a warning' do
+        find("#note_#{existing_note.id} .js-note-edit").click
 
-      expect(page).to have_selector(".alert")
-    end
+        expect(page).to have_field("note[note]", with: note_text)
+
+        find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+        update_note(existing_note, updated_text)
 
-    it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
-      find("#note_#{existing_note.id} .js-note-edit").click
+        expect(page).to have_selector(".alert")
+      end
+
+      it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do
+        find("#note_#{existing_note.id} .js-note-edit").click
+
+        expect(page).to have_field("note[note]", with: note_text)
+
+        find("#note_#{existing_note.id} .js-note-text").set('something random')
+
+        update_note(existing_note, updated_text)
+
+        find("#note_#{existing_note.id} .note-edit-cancel").click
+
+        expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+      end
+    end
 
-      expect(page).to have_field("note[note]", with: note_text)
+    context 'when from another user' do
+      let(:user1) { create(:user) }
+      let(:user2) { create(:user) }
+      let(:note_text) { "Hello World" }
+      let(:updated_text) { "Bye World" }
+      let!(:existing_note) { create(:note, noteable: issue, project: project, author: user1, note: note_text) }
 
-      find("#note_#{existing_note.id} .js-note-text").set('something random')
+      before do
+        login_as(user2)
+        visit namespace_project_issue_path(project.namespace, project, issue)
+      end
 
-      update_note(existing_note, updated_text)
+      it 'has .original-note-content to compare against' do
+        expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
+        expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", visible: false)
 
-      find("#note_#{existing_note.id} .note-edit-cancel").click
+        update_note(existing_note, updated_text)
 
-      expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+        expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
+        expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", visible: false)
+      end
     end
   end
 
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 87745ea981..7f12dea527 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -99,8 +99,6 @@ import '~/notes';
 
         notes = jasmine.createSpyObj('notes', [
           'refresh',
-          'isNewNote',
-          'isUpdatedNote',
           'collapseLongCommitList',
           'updateNotesCount',
           'putConflictEditWarningInPlace'
@@ -110,13 +108,15 @@ import '~/notes';
         notes.updatedNotesTrackingMap = {};
 
         spyOn(gl.utils, 'localTimeAgo');
+        spyOn(Notes, 'isNewNote').and.callThrough();
+        spyOn(Notes, 'isUpdatedNote').and.callThrough();
         spyOn(Notes, 'animateAppendNote').and.callThrough();
         spyOn(Notes, 'animateUpdateNote').and.callThrough();
       });
 
       describe('when adding note', () => {
         it('should call .animateAppendNote', () => {
-          notes.isNewNote.and.returnValue(true);
+          Notes.isNewNote.and.returnValue(true);
           Notes.prototype.renderNote.call(notes, note, null, $notesList);
 
           expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
@@ -125,7 +125,8 @@ import '~/notes';
 
       describe('when note was edited', () => {
         it('should call .animateUpdateNote', () => {
-          notes.isUpdatedNote.and.returnValue(true);
+          Notes.isNewNote.and.returnValue(false);
+          Notes.isUpdatedNote.and.returnValue(true);
           const $note = $('<div>');
           $notesList.find.and.returnValue($note);
           Notes.prototype.renderNote.call(notes, note, null, $notesList);
@@ -135,7 +136,8 @@ import '~/notes';
 
         describe('while editing', () => {
           it('should update textarea if nothing has been touched', () => {
-            notes.isUpdatedNote.and.returnValue(true);
+            Notes.isNewNote.and.returnValue(false);
+            Notes.isUpdatedNote.and.returnValue(true);
             const $note = $(`<div class="is-editing">
               <div class="original-note-content">initial</div>
               <textarea class="js-note-text">initial</textarea>
@@ -147,7 +149,8 @@ import '~/notes';
           });
 
           it('should call .putConflictEditWarningInPlace', () => {
-            notes.isUpdatedNote.and.returnValue(true);
+            Notes.isNewNote.and.returnValue(false);
+            Notes.isUpdatedNote.and.returnValue(true);
             const $note = $(`<div class="is-editing">
               <div class="original-note-content">initial</div>
               <textarea class="js-note-text">different</textarea>
@@ -161,6 +164,47 @@ import '~/notes';
       });
     });
 
+    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;
@@ -180,15 +224,15 @@ import '~/notes';
         row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
 
         notes = jasmine.createSpyObj('notes', [
-          'isNewNote',
           'isParallelView',
           'updateNotesCount',
         ]);
         notes.note_ids = [];
 
         spyOn(gl.utils, 'localTimeAgo');
+        spyOn(Notes, 'isNewNote');
         spyOn(Notes, 'animateAppendNote');
-        notes.isNewNote.and.returnValue(true);
+        Notes.isNewNote.and.returnValue(true);
         notes.isParallelView.and.returnValue(false);
         row.prevAll.and.returnValue(row);
         row.first.and.returnValue(row);
-- 
2.30.9