From 90a6be190feea0966e9ed9b6731d930bcff32d68 Mon Sep 17 00:00:00 2001
From: Sean McGivern <sean@gitlab.com>
Date: Wed, 6 Jul 2016 12:36:39 +0100
Subject: [PATCH] Ensure only renderable text diffs are collapsed

Other diffs (those that are too large to render anyway, image diffs,
diffs suppressed by .gitattributes) should be rendered immediately.
---
 app/assets/javascripts/single_diff.js.coffee  |  10 +-
 app/assets/stylesheets/framework/files.scss   |   4 +
 app/helpers/diff_helper.rb                    |   1 +
 app/views/projects/diffs/_content.html.haml   |   6 +-
 app/views/projects/diffs/_file.html.haml      |   7 +-
 .../expand_collapse_diffs_spec.rb             | 138 ++++++++++++++++++
 .../labels/update_prioritization_spec.rb      |   2 +-
 spec/support/capybara_helpers.rb              |   8 +
 8 files changed, 166 insertions(+), 10 deletions(-)
 create mode 100644 spec/features/merge_requests/expand_collapse_diffs_spec.rb

diff --git a/app/assets/javascripts/single_diff.js.coffee b/app/assets/javascripts/single_diff.js.coffee
index 4d1c28c082b..76db716317b 100644
--- a/app/assets/javascripts/single_diff.js.coffee
+++ b/app/assets/javascripts/single_diff.js.coffee
@@ -2,13 +2,15 @@ class @SingleDiff
 
   LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'
   ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'
+  COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'
 
   constructor: (@file) ->
     @content = $('.diff-content', @file)
-    @diffForPath = @content.data 'diff-for-path'
+    @diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path'
     @setOpenState()
 
     $('.file-title > a', @file).on 'click', @toggleDiff
+    @enableToggleOnContent()
 
   setOpenState: ->
     if @diffForPath
@@ -18,11 +20,15 @@ class @SingleDiff
       @contentHTML = @content.html()
     return
 
+  enableToggleOnContent: ->
+    @content.find('.nothing-here-block.diff-collapsed').on 'click', @toggleDiff
+
   toggleDiff: (e) =>
     e.preventDefault()
     @isOpen = !@isOpen
     if not @isOpen and not @hasError
-      @content.empty()
+      @content.html COLLAPSED_HTML
+      @enableToggleOnContent
       return
     if @contentHTML
       @setContentHTML()
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 71e4b50f2af..02480689f09 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -189,3 +189,7 @@ span.idiff {
     border-bottom-right-radius: 2px;
   }
 }
+
+.nothing-here-block.diff-collapsed {
+  cursor: pointer;
+}
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 9a5920edfa2..93f12198a58 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -15,6 +15,7 @@ module DiffHelper
 
     diff_commit = commit_for_diff(diff_file)
     blob = project.repository.blob_for_diff(diff_commit, diff_file)
+    @expand_all = true
 
     locals = {
       diff_file: diff_file,
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 832aa0c5a14..9c6a17c0a8c 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -9,7 +9,11 @@
     - if !project.repository.diffable?(blob)
       .nothing-here-block This diff was suppressed by a .gitattributes entry.
     - elsif diff_file.diff_lines.length > 0
-      - if diff_view == 'parallel'
+      - if diff_file.collapsed_by_default? && !@expand_all
+        - url = url_for(params.merge(action: :diff_for_path, path: diff_file.file_path, format: nil))
+        .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
+          This diff is collapsed. Click to expand it.
+      - elsif diff_view == 'parallel'
         = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
       - else
         = render "projects/diffs/text_file", diff_file: diff_file
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index c83ed55efe1..c306909fb1a 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -16,9 +16,4 @@
 
         = view_file_btn(diff_commit.id, diff_file, project)
 
-  - if diff_file.collapsed_by_default?
-    - url = url_for(params.merge(action: :diff_for_path, path: diff_file.file_path, format: nil))
-    .diff-content.diff-wrap-lines{data: { diff_for_path: url }}
-      .nothing-here-block File hidden by default; content for this element available at #{url}
-  - else
-    = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project
+  = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project
diff --git a/spec/features/merge_requests/expand_collapse_diffs_spec.rb b/spec/features/merge_requests/expand_collapse_diffs_spec.rb
new file mode 100644
index 00000000000..173ea3720da
--- /dev/null
+++ b/spec/features/merge_requests/expand_collapse_diffs_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+feature 'Expand and collapse diffs', js: true, feature: true do
+  include WaitForAjax
+
+  before do
+    login_as :admin
+    merge_request = create(:merge_request, source_branch: 'expand-collapse-diffs', target_branch: 'master')
+    project = merge_request.source_project
+
+    # Ensure that undiffable.md is in .gitattributes
+    project.repository.copy_gitattributes('expand-collapse-diffs')
+    visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+    execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
+  end
+
+  def file_container(filename)
+    find("[data-blob-diff-path*='#{filename}']")
+  end
+
+  # Use define_method instead of let (which is memoized) so that this just works across a
+  # reload.
+  #
+  ['small_diff.md', 'large_diff.md', 'undiffable.md', 'too_large.md', 'too_large_image.jpg'].each do |file|
+    define_method(file.split('.').first) { file_container(file) }
+  end
+
+  context 'visiting an existing merge request' do
+    it 'shows small diffs immediately' do
+      expect(small_diff).to have_selector('.code')
+      expect(small_diff).not_to have_selector('.nothing-here-block')
+    end
+
+    it 'collapses larges diffs by default' do
+      expect(large_diff).not_to have_selector('.code')
+      expect(large_diff).to have_selector('.nothing-here-block')
+    end
+
+    it 'shows non-renderable diffs as such immediately, regardless of their size' do
+      expect(undiffable).not_to have_selector('.code')
+      expect(undiffable).to have_selector('.nothing-here-block')
+      expect(undiffable).to have_content('gitattributes')
+    end
+
+    it 'does not allow diffs that are larger than the maximum size to be expanded' do
+      expect(too_large).not_to have_selector('.code')
+      expect(too_large).to have_selector('.nothing-here-block')
+      expect(too_large).to have_content('too large')
+    end
+
+    it 'shows image diffs immediately, regardless of their size' do
+      expect(too_large_image).not_to have_selector('.nothing-here-block')
+      expect(too_large_image).to have_selector('.image')
+    end
+
+    context 'expanding a large diff' do
+      before do
+        click_link('large_diff.md')
+        wait_for_ajax
+      end
+
+      it 'makes a request to get the content' do
+        ajax_uris = evaluate_script('ajaxUris')
+
+        expect(ajax_uris).not_to be_empty
+        expect(ajax_uris.first).to include('large_diff.md')
+      end
+
+      it 'shows the diff content' do
+        expect(large_diff).to have_selector('.code')
+        expect(large_diff).not_to have_selector('.nothing-here-block')
+      end
+
+      context 'adding a comment to the expanded diff' do
+        let(:comment_text) { 'A comment' }
+
+        before do
+          large_diff.find('.line_holder', match: :prefer_exact).hover
+          large_diff.find('.add-diff-note').click
+          large_diff.find('.note-textarea').send_keys comment_text
+          large_diff.find_button('Comment').click
+          wait_for_ajax
+        end
+
+        it 'adds the comment' do
+          expect(large_diff.find('.notes')).to have_content comment_text
+        end
+
+        context 'reloading the page' do
+          before { refresh }
+
+          it 'collapses the large diff by default' do
+            expect(large_diff).not_to have_selector('.code')
+            expect(large_diff).to have_selector('.nothing-here-block')
+          end
+
+          context 'expanding the diff' do
+            before do
+              click_link('large_diff.md')
+              wait_for_ajax
+            end
+
+            it 'shows the diff content' do
+              expect(large_diff).to have_selector('.code')
+              expect(large_diff).not_to have_selector('.nothing-here-block')
+            end
+
+            it 'shows the diff comment' do
+              expect(large_diff.find('.notes')).to have_content comment_text
+            end
+          end
+        end
+      end
+    end
+
+    context 'collapsing an expanded diff' do
+      before { click_link('small_diff.md') }
+
+      it 'hides the diff content' do
+        expect(small_diff).not_to have_selector('.code')
+        expect(small_diff).to have_selector('.nothing-here-block')
+      end
+
+      context 're-expanding the same diff' do
+        before { click_link('small_diff.md') }
+
+        it 'shows the diff content' do
+          expect(small_diff).to have_selector('.code')
+          expect(small_diff).not_to have_selector('.nothing-here-block')
+        end
+
+        it 'does not make a new HTTP request' do
+          expect(evaluate_script('ajaxUris')).to be_empty
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 6a39c302f55..98ba93b4036 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do
         expect(page.all('li').last).to have_content('bug')
       end
 
-      visit current_url
+      refresh
       wait_for_ajax
 
       page.within('.prioritized-labels') do
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 9b5c3065eed..b57a3493aff 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -27,6 +27,14 @@ module CapybaraHelpers
       end
     end
   end
+
+  # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
+  #
+  def refresh
+    url = current_url
+    visit 'about:blank'
+    visit url
+  end
 end
 
 RSpec.configure do |config|
-- 
GitLab