summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2017-04-28 07:31:45 +0000
committerTim Zallmann <tzallmann@gitlab.com>2017-04-28 07:31:45 +0000
commit6dc5c32050faaa2a56c65c88cf8bd213507ada4a (patch)
treebef3265fd61060cd4dc3ba0f2b7b1663a1937a4a /spec
parent479d21d5850d9878ef292016ae7e643fdbb0b796 (diff)
parentbb7865276bb9adfe34755c3ea358e4951ec37d6f (diff)
downloadgitlab-ce-6dc5c32050faaa2a56c65c88cf8bd213507ada4a.tar.gz
Merge branch 'dm-blob-viewers' into 'master'
Refactor rich blob viewers and add Code|Rendered switch Closes #24287 See merge request !10566
Diffstat (limited to 'spec')
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb312
-rw-r--r--spec/features/projects/files/browse_files_spec.rb5
-rw-r--r--spec/helpers/blob_helper_spec.rb120
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js161
-rw-r--r--spec/javascripts/fixtures/blob.rb29
-rw-r--r--spec/models/blob_spec.rb258
-rw-r--r--spec/models/blob_viewer/base_spec.rb186
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb50
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb96
10 files changed, 1066 insertions, 153 deletions
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 344e31e5ef5..f197fb44608 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -479,6 +479,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a blob' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ wait_for_ajax
end
context 'selecting one word of text' do
@@ -520,6 +521,7 @@ describe 'Copy as GFM', feature: true, js: true do
context 'from a GFM code block' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ wait_for_ajax
end
context 'selecting one word of text' do
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 7cfa5b9716f..cc11cb7a55f 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,21 +1,313 @@
require 'spec_helper'
-feature 'File blob', feature: true do
+feature 'File blob', :js, feature: true do
include TreeHelper
+ include WaitForAjax
- let(:project) { create(:project, :public, :test_repo) }
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
- let(:branch) { 'master' }
- let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
+ let(:project) { create(:project, :public) }
- context 'anonymous' do
- context 'from blob file path' do
+ def visit_blob(path, fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ context 'Ruby file' do
+ before do
+ visit_blob('files/ruby/popen.rb')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows highlighted Ruby code
+ expect(page).to have_content("require 'fileutils'")
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+
+ context 'Markdown file' do
+ context 'visiting directly' do
before do
- visit namespace_project_blob_path(project.namespace, project, tree_join(branch, file_path))
+ visit_blob('files/markdown/ruby-style-guide.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows rendered Markdown
+ expect(page).to have_link("PEP-8")
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # shows a disabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+
+ context 'switching to the rich viewer again' do
+ before do
+ find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
end
+ end
+
+ context 'visiting with a line number anchor' do
+ before do
+ visit_blob('files/markdown/ruby-style-guide.md', 'L1')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # highlights the line in question
+ expect(page).to have_selector('#LC1.hll')
+
+ # shows highlighted Markdown code
+ expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'Markdown file (stored in LFS)' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add Markdown in LFS",
+ file_path: 'files/lfs/file.md',
+ file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data
+ ).execute
+ end
+
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the simple viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]')
+
+ # shows an error message
+ expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can view the source or download it instead.')
+
+ # shows a viewer switcher
+ expect(page).to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+
+ context 'switching to the simple viewer' do
+ before do
+ find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click
+
+ wait_for_ajax
+ end
+
+ it 'displays an error' do
+ aggregate_failures do
+ # hides the rich viewer
+ expect(page).to have_selector('.blob-viewer[data-type="simple"]')
+ expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
+
+ # shows an error message
+ expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/file.md')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'PDF file' do
+ before do
+ project.add_master(project.creator)
+
+ Files::CreateService.new(
+ project,
+ project.creator,
+ start_branch: 'master',
+ branch_name: 'master',
+ commit_message: "Add PDF",
+ file_path: 'files/test.pdf',
+ file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf'))
+ ).execute
+
+ visit_blob('files/test.pdf')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows rendered PDF
+ expect(page).to have_selector('.js-pdf-viewer')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+
+ context 'ISO file (stored in LFS)' do
+ context 'when LFS is enabled on the project' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (1.5 MB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
+ end
+ end
+ end
+
+ context 'when LFS is disabled on the project' do
+ before do
+ visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows text
+ expect(page).to have_content('size 1575078')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
+
+ # shows an enabled copy button
+ expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ end
+ end
+ end
+ end
+
+ context 'ZIP file' do
+ before do
+ visit_blob('Gemfile.zip')
+
+ wait_for_ajax
+ end
+
+ it 'displays the blob' do
+ aggregate_failures do
+ # shows a download link
+ expect(page).to have_link('Download (2.11 KB)')
+
+ # does not show a viewer switcher
+ expect(page).not_to have_selector('.js-blob-viewer-switcher')
- it 'updates content' do
- expect(page).to have_link 'Edit'
+ # does not show a copy button
+ expect(page).not_to have_selector('.js-copy-blob-source-btn')
end
end
end
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index d281043caa3..70e96efd557 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', feature: true do
+feature 'user browses project', feature: true, js: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,7 +13,7 @@ feature 'user browses project', feature: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
click_link 'Blame'
-
+
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
expect(page).to have_content "Initial commit"
@@ -24,6 +24,7 @@ feature 'user browses project', feature: true do
click_link 'files'
click_link 'lfs'
click_link 'lfs_object.iso'
+ wait_for_ajax
expect(page).not_to have_content 'Download (1.5 MB)'
expect(page).to have_content 'version https://git-lfs.github.com/spec/v1'
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 508aeb7cf67..379f62f73e1 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -56,15 +56,14 @@ describe BlobHelper do
end
end
- describe "#sanitize_svg" do
+ describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read }
it 'retains essential elements' do
- blob = OpenStruct.new(data: data)
- expect(sanitize_svg(blob).data).to eq(expected)
+ expect(sanitize_svg_data(data)).to eq(expected)
end
end
@@ -105,4 +104,119 @@ describe BlobHelper do
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end
end
+
+ context 'viewer related' do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.type = :rich
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ describe '#blob_render_error_reason' do
+ context 'for error :too_large' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 5 MB')
+ end
+ end
+
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is larger than 1 MB')
+ end
+ end
+ end
+
+ context 'for error :server_side_but_stored_in_lfs' do
+ let(:blob) { fake_blob(lfs: true) }
+
+ it 'returns an error message' do
+ expect(helper.blob_render_error_reason(viewer)).to eq('it is stored in LFS')
+ end
+ end
+ end
+
+ describe '#blob_render_error_options' do
+ before do
+ assign(:project, project)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ context 'for error :too_large' do
+ context 'when the max size can be overridden' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
+
+ it 'includes a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
+ end
+ end
+
+ context 'when the max size cannot be overridden' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
+ end
+ end
+ end
+
+ context 'when the viewer is rich' do
+ context 'the blob is rendered as text' do
+ let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+
+ it 'includes a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/view the source/)
+ end
+ end
+
+ context 'the blob is not rendered as text' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, lfs: true) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+ end
+
+ context 'when the viewer is not rich' do
+ before do
+ viewer_class.type = :simple
+ end
+
+ let(:blob) { fake_blob(path: 'file.md', lfs: true) }
+
+ it 'does not include a "view the source" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/view the source/)
+ end
+ end
+
+ it 'includes a "download it" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/download it/)
+ end
+ end
+ end
end
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
new file mode 100644
index 00000000000..13f122b68b2
--- /dev/null
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -0,0 +1,161 @@
+/* eslint-disable no-new */
+import BlobViewer from '~/blob/viewer/index';
+
+describe('Blob viewer', () => {
+ let blob;
+ preloadFixtures('blob/show.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html.raw');
+ $('#modal-upload-blob').remove();
+
+ blob = new BlobViewer();
+
+ spyOn($, 'ajax').and.callFake(() => {
+ const d = $.Deferred();
+
+ d.resolve({
+ html: '<div>testing</div>',
+ });
+
+ return d.promise();
+ });
+ });
+
+ afterEach(() => {
+ location.hash = '';
+ });
+
+ it('loads source file after switching views', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('loads source file when line number is in hash', (done) => {
+ location.hash = '#L1';
+
+ new BlobViewer();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
+ .classList.contains('hidden'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('doesnt reload file if already loaded', (done) => {
+ const asyncClick = () => new Promise((resolve) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(resolve);
+ });
+
+ asyncClick()
+ .then(() => {
+ expect($.ajax).toHaveBeenCalled();
+ return asyncClick();
+ })
+ .then(() => {
+ expect($.ajax.calls.count()).toBe(1);
+ expect(
+ document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
+ ).toBe('true');
+
+ done();
+ })
+ .catch(() => {
+ fail();
+ done();
+ });
+ });
+
+ describe('copy blob button', () => {
+ it('disabled on load', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeTruthy();
+ });
+
+ it('has tooltip when disabled', () => {
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Switch to the source to copy it to the clipboard');
+ });
+
+ it('enables after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'),
+ ).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('updates tooltip after switching to simple view', (done) => {
+ document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
+
+ setTimeout(() => {
+ expect($.ajax).toHaveBeenCalled();
+
+ expect(
+ document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'),
+ ).toBe('Copy source to clipboard');
+
+ done();
+ });
+ });
+ });
+
+ describe('switchToViewer', () => {
+ it('removes active class from old viewer button', () => {
+ blob.switchToViewer('simple');
+
+ expect(
+ document.querySelector('.js-blob-viewer-switch-btn.active[data-viewer="rich"]'),
+ ).toBeNull();
+ });
+
+ it('adds active class to new viewer button', () => {
+ const simpleBtn = document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]');
+
+ spyOn(simpleBtn, 'blur');
+
+ blob.switchToViewer('simple');
+
+ expect(
+ simpleBtn.classList.contains('active'),
+ ).toBeTruthy();
+ expect(simpleBtn.blur).toHaveBeenCalled();
+ });
+
+ it('sends AJAX request when switching to simple view', () => {
+ blob.switchToViewer('simple');
+
+ expect($.ajax).toHaveBeenCalled();
+ });
+
+ it('does not send AJAX request when switching to rich view', () => {
+ blob.switchToViewer('simple');
+ blob.switchToViewer('rich');
+
+ expect($.ajax.calls.count()).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
new file mode 100644
index 00000000000..16490ad5039
--- /dev/null
+++ b/spec/javascripts/fixtures/blob.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('blob/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'blob/show.html.raw' do |example|
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'add-ipython-files/files/ipython/basic.ipynb')
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index e5dd57fc4bb..7e8a1c8add7 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -2,6 +2,14 @@
require 'rails_helper'
describe Blob do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
describe '.decorate' do
it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil
@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do
it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3"
- blob = described_class.new(double(binary?: true, data: data))
+ blob = fake_blob(binary: true, data: data)
expect(blob.data).to eq(data)
end
@@ -20,202 +28,176 @@ describe Blob do
context 'using a text blob' do
it 'converts the data to UTF-8' do
- blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+ blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
expect(blob.data).to eq("\n���")
end
end
end
- describe '#svg?' do
- it 'is falsey when not text' do
- git_blob = double(text?: false)
+ describe '#raw_binary?' do
+ context 'if the blob is a valid LFS pointer' do
+ context 'if the extension has a rich viewer' do
+ context 'if the viewer is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
-
- it 'is falsey when no language is detected' do
- git_blob = double(text?: true, language: nil)
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
+ context 'if the viewer is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md', lfs: true)
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
-
- expect(described_class.decorate(git_blob)).not_to be_svg
- end
-
- it 'is truthy when language is SVG' do
- git_blob = double(text?: true, language: double(name: 'SVG'))
-
- expect(described_class.decorate(git_blob)).to be_svg
- end
- end
-
- describe '#pdf?' do
- it 'is falsey when file extension is not .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.txt')
-
- expect(described_class.decorate(git_blob)).not_to be_pdf
- end
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
- it 'is truthy when file extension is .pdf' do
- git_blob = Gitlab::Git::Blob.new(name: 'git_blob.pdf')
+ context "if the extension doesn't have a rich viewer" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.exe', lfs: true)
- expect(described_class.decorate(git_blob)).to be_pdf
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
end
- end
- describe '#ipython_notebook?' do
- it 'is falsey when language is not Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'JSON'))
+ context 'if the blob is not an LFS pointer' do
+ context 'if the blob is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
- end
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
- it 'is truthy when language is Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+ context 'if the blob is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_ipython_notebook
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
end
end
- describe '#sketch?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.png")
-
- expect(described_class.decorate(git_blob)).not_to be_sketch
- end
-
- it 'is truthy with sketch extension' do
- git_blob = Gitlab::Git::Blob.new(name: "design.sketch")
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_sketch
+ expect(blob.extension).to eq('md')
end
end
- describe '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ describe '#simple_viewer' do
+ context 'when the blob is empty' do
+ it 'returns an empty viewer' do
+ blob = fake_blob(data: '')
- expect(described_class.decorate(git_blob)).not_to be_video
- end
-
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "is truthy when extension is .#{ext}" do
- git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
-
- expect(described_class.decorate(git_blob)).to be_video
+ expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
end
end
- end
- describe '#stl?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.png')
+ context 'when the file represented by the blob is binary' do
+ it 'returns a download viewer' do
+ blob = fake_blob(binary: true)
- expect(described_class.decorate(git_blob)).not_to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Download)
+ end
end
- it 'is truthy with STL extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
+ context 'when the file represented by the blob is text-based' do
+ it 'returns a text viewer' do
+ blob = fake_blob
- expect(described_class.decorate(git_blob)).to be_stl
+ expect(blob.simple_viewer).to be_a(BlobViewer::Text)
+ end
end
end
- describe '#to_partial_path' do
- let(:project) { double(lfs_enabled?: true) }
+ describe '#rich_viewer' do
+ context 'when the blob is an invalid LFS pointer' do
+ before do
+ project.lfs_enabled = false
+ end
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- name: nil,
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false,
- binary?: false,
- stl?: false
- )
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- described_class.decorate(Gitlab::Git::Blob.new({})).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.rich_viewer).to be_nil
end
end
- it 'handles LFS pointers with LFS enabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- expect(blob.to_partial_path(project)).to eq 'download'
- end
-
- it 'handles LFS pointers with LFS disabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- project = double(lfs_enabled?: false)
- expect(blob.to_partial_path(project)).to eq 'text'
- end
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'svg'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
- end
+ context 'when the blob is a valid LFS pointer' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- it 'handles text' do
- blob = stubbed_blob(text?: true, name: 'test.txt')
- expect(blob.to_partial_path(project)).to eq 'text'
- end
-
- it 'defaults to download' do
- blob = stubbed_blob
- expect(blob.to_partial_path(project)).to eq 'download'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles PDFs' do
- blob = stubbed_blob(name: 'blob.pdf', pdf?: true)
- expect(blob.to_partial_path(project)).to eq 'pdf'
- end
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- it 'handles iPython notebooks' do
- blob = stubbed_blob(text?: true, ipython_notebook?: true)
- expect(blob.to_partial_path(project)).to eq 'notebook'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles Sketch files' do
- blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
- expect(blob.to_partial_path(project)).to eq 'sketch'
- end
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
- it 'handles STLs' do
- blob = stubbed_blob(text?: true, stl?: true)
- expect(blob.to_partial_path(project)).to eq 'stl'
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#rendered_as_text?' do
+ context 'when ignoring errors' do
+ context 'when the simple viewer is text-based' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- it 'returns true when the blob size is smaller than the SVG limit' do
- expect(blob).to receive(:size).and_return(42)
+ expect(blob.rendered_as_text?).to be_truthy
+ end
+ end
+
+ context 'when the simple viewer is binary' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.rendered_as_text?).to be_falsey
+ end
+ end
end
- it 'returns true when the blob size is equal to the SVG limit' do
- expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+ context 'when not ignoring errors' do
+ context 'when the viewer has render errors' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
- end
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
- it 'returns false when the blob size is larger than the SVG limit' do
- expect(blob).to receive(:size).and_return(1.terabyte)
+ context "when the viewer doesn't have render errors" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md')
- expect(blob.size_within_svg_limits?).to eq(false)
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
+ end
+ end
end
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
new file mode 100644
index 00000000000..a3e598de56d
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,186 @@
+require 'spec_helper'
+
+describe BlobViewer::Base, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(described_class) do
+ self.extensions = %w(pdf)
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+
+ describe '.can_render?' do
+ context 'when the extension is supported' do
+ let(:blob) { fake_blob(path: 'file.pdf') }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the extension is not supported' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ describe '#too_large?' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#absolutely_too_large?' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.absolutely_too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.absolutely_too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#can_override_max_size?' do
+ context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.can_override_max_size?).to be_truthy
+ end
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the max size is overridden' do
+ before do
+ viewer.override_max_size = true
+ end
+
+ context 'when the blob size is larger than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the absolute max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the max size is not overridden' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the viewer is server side but the blob is stored in LFS' do
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'return :server_side_but_stored_in_lfs' do
+ expect(viewer.render_error).to eq(:server_side_but_stored_in_lfs)
+ end
+ end
+ end
+
+ describe '#prepare!' do
+ context 'when the viewer is server side' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ before do
+ viewer_class.client_side = false
+ end
+
+ it 'loads all blob data' do
+ expect(blob).to receive(:load_all_data!)
+
+ viewer.prepare!
+ end
+ end
+
+ context 'when the viewer is client side' do
+ let(:blob) { fake_blob(path: 'file.md') }
+
+ before do
+ viewer_class.client_side = true
+ end
+
+ it "doesn't load all blob data" do
+ expect(blob).not_to receive(:load_all_data!)
+
+ viewer.prepare!
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
new file mode 100644
index 00000000000..b29af732ad3
--- /dev/null
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -0,0 +1,50 @@
+module FakeBlobHelpers
+ class FakeBlob
+ include Linguist::BlobHelper
+
+ attr_reader :path, :size, :data, :lfs_oid, :lfs_size
+
+ def initialize(path: 'file.txt', size: 1.kilobyte, data: 'foo', binary: false, lfs: nil)
+ @path = path
+ @size = size
+ @data = data
+ @binary = binary
+
+ @lfs_pointer = lfs.present?
+ if @lfs_pointer
+ @lfs_oid = SecureRandom.hex(20)
+ @lfs_size = 1.megabyte
+ end
+ end
+
+ alias_method :name, :path
+
+ def mode
+ nil
+ end
+
+ def id
+ 0
+ end
+
+ def binary?
+ @binary
+ end
+
+ def load_all_data!(repository)
+ # No-op
+ end
+
+ def lfs_pointer?
+ @lfs_pointer
+ end
+
+ def truncated?
+ false
+ end
+ end
+
+ def fake_blob(**kwargs)
+ Blob.decorate(FakeBlob.new(**kwargs), project)
+ end
+end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
new file mode 100644
index 00000000000..a4915264abe
--- /dev/null
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe 'projects/blob/_viewer.html.haml', :view do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::Rich
+
+ self.partial_name = 'text'
+ self.max_size = 1.megabyte
+ self.absolute_max_size = 5.megabytes
+ self.client_side = false
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+ let(:blob) { fake_blob }
+
+ before do
+ assign(:project, project)
+ assign(:id, File.join('master', blob.path))
+
+ controller.params[:controller] = 'projects/blob'
+ controller.params[:action] = 'show'
+ controller.params[:namespace_id] = project.namespace.to_param
+ controller.params[:project_id] = project.to_param
+ controller.params[:id] = File.join('master', blob.path)
+ end
+
+ def render_view
+ render partial: 'projects/blob/viewer', locals: { viewer: viewer }
+ end
+
+ context 'when the viewer is server side' do
+ before do
+ viewer_class.client_side = false
+ end
+
+ context 'when there is no render error' do
+ it 'adds a URL to the blob viewer element' do
+ render_view
+
+ expect(rendered).to have_css('.blob-viewer[data-url]')
+ end
+
+ it 'displays a spinner' do
+ render_view
+
+ expect(rendered).to have_css('i[aria-label="Loading content"]')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+
+ context 'when the viewer is client side' do
+ before do
+ viewer_class.client_side = true
+ end
+
+ context 'when there is no render error' do
+ it 'prepares the viewer' do
+ expect(viewer).to receive(:prepare!)
+
+ render_view
+ end
+
+ it 'renders the viewer' do
+ render_view
+
+ expect(view).to render_template('projects/blob/viewers/_text')
+ end
+ end
+
+ context 'when there is a render error' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
+
+ it 'renders the error' do
+ render_view
+
+ expect(view).to render_template('projects/blob/_render_error')
+ end
+ end
+ end
+end