Commit 327013ac authored by Robert Speicher's avatar Robert Speicher

Merge branch 'rs-markdown-feature-pipelines' into 'master'

Prepare Markdown feature spec for testing multiple pipelines

This MR lays the groundwork for including or excluding certain HTML::Pipeline
filters depending on what we're parsing. For example, excluding reference
filters in the Issues and Merge Requests titles.

See merge request !1053
parents a876db48 8c957b54
......@@ -17,410 +17,215 @@ require 'erb'
# -> Post-process HTML
# -> `gfm_with_options` helper
# -> HTML::Pipeline
# -> Sanitize
# -> RelativeLink
# -> Emoji
# -> Table of Contents
# -> Autolinks
# -> Rinku (http, https, ftp)
# -> Other schemes
# -> ExternalLink
# -> References
# -> TaskList
# -> SanitizationFilter
# -> Other filters, depending on pipeline
# -> `html_safe`
# -> Template
#
# See the MarkdownFeature class for setup details.
describe 'GitLab Markdown', feature: true do
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include Capybara::Node::Matchers
include GitlabMarkdownHelper
include MarkdownMatchers
# `markdown` calls these two methods
def current_user
@feat.user
end
def user_color_scheme_class
:white
end
# Let's only parse this thing once
before(:all) do
@feat = MarkdownFeature.new
# `markdown` expects a `@project` variable
@project = @feat.project
@md = markdown(@feat.raw_markdown)
@doc = Nokogiri::HTML::DocumentFragment.parse(@md)
end
after(:all) do
@feat.teardown
# Sometimes it can be useful to see the parsed output of the Markdown document
# for debugging. Call this method to write the output to
# `tmp/capybara/<filename>.html`.
def write_markdown(filename = 'markdown_spec')
File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file|
file.puts @html
end
end
# Given a header ID, goes to that element's parent (the header itself), then
# its next sibling element (the body).
def get_section(id)
@doc.at_css("##{id}").parent.next_element
def doc(html = @html)
Nokogiri::HTML::DocumentFragment.parse(html)
end
# Sometimes it can be useful to see the parsed output of the Markdown document
# for debugging. Uncomment this block to write the output to
# tmp/capybara/markdown_spec.html.
#
# it 'writes to a file' do
# File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file|
# file.puts @md
# end
# end
describe 'Markdown' do
describe 'No Intra Emphasis' do
# Shared behavior that all pipelines should exhibit
shared_examples 'all pipelines' do
describe 'Redcarpet extensions' do
it 'does not parse emphasis inside of words' do
body = get_section('no-intra-emphasis')
expect(body.to_html).not_to match('foo<em>bar</em>baz')
expect(doc.to_html).not_to match('foo<em>bar</em>baz')
end
end
describe 'Tables' do
it 'parses table Markdown' do
body = get_section('tables')
expect(body).to have_selector('th:contains("Header")')
expect(body).to have_selector('th:contains("Row")')
expect(body).to have_selector('th:contains("Example")')
aggregate_failures do
expect(doc).to have_selector('th:contains("Header")')
expect(doc).to have_selector('th:contains("Row")')
expect(doc).to have_selector('th:contains("Example")')
end
end
it 'allows Markdown in tables' do
expect(@doc.at_css('td:contains("Baz")').children.to_html).
expect(doc.at_css('td:contains("Baz")').children.to_html).
to eq '<strong>Baz</strong>'
end
end
describe 'Fenced Code Blocks' do
it 'parses fenced code blocks' do
expect(@doc).to have_selector('pre.code.highlight.white.c')
expect(@doc).to have_selector('pre.code.highlight.white.python')
aggregate_failures do
expect(doc).to have_selector('pre.code.highlight.white.c')
expect(doc).to have_selector('pre.code.highlight.white.python')
end
end
end
describe 'Strikethrough' do
it 'parses strikethroughs' do
expect(@doc).to have_selector(%{del:contains("and this text doesn't")})
expect(doc).to have_selector(%{del:contains("and this text doesn't")})
end
end
describe 'Superscript' do
it 'parses superscript' do
body = get_section('superscript')
expect(body.to_html).to match('1<sup>st</sup>')
expect(body.to_html).to match('2<sup>nd</sup>')
expect(doc).to have_selector('sup', count: 2)
end
end
end
describe 'HTML::Pipeline' do
describe 'SanitizationFilter' do
it 'uses a permissive whitelist' do
expect(@doc).to have_selector('b:contains("b tag")')
expect(@doc).to have_selector('em:contains("em tag")')
expect(@doc).to have_selector('code:contains("code tag")')
expect(@doc).to have_selector('kbd:contains("s")')
expect(@doc).to have_selector('strike:contains(Emoji)')
expect(@doc).to have_selector('img[src*="smile.png"]')
expect(@doc).to have_selector('br')
expect(@doc).to have_selector('hr')
it 'permits b elements' do
expect(doc).to have_selector('b:contains("b tag")')
end
it 'permits span elements' do
expect(@doc).to have_selector('span:contains("span tag")')
it 'permits em elements' do
expect(doc).to have_selector('em:contains("em tag")')
end
it 'permits table alignment' do
expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
it 'permits code elements' do
expect(doc).to have_selector('code:contains("code tag")')
end
it 'removes `rel` attribute from links' do
body = get_section('sanitizationfilter')
expect(body).not_to have_selector('a[rel="bookmark"]')
it 'permits kbd elements' do
expect(doc).to have_selector('kbd:contains("s")')
end
it "removes `href` from `a` elements if it's fishy" do
expect(@doc).not_to have_selector('a[href*="javascript"]')
it 'permits strike elements' do
expect(doc).to have_selector('strike:contains(Emoji)')
end
end
describe 'Escaping' do
let(:table) { @doc.css('table').last.at_css('tbody') }
it 'escapes non-tag angle brackets' do
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
it 'permits img elements' do
expect(doc).to have_selector('img[src*="smile.png"]')
end
end
describe 'Edge Cases' do
it 'allows markup inside link elements' do
expect(@doc.at_css('a[href="#link-emphasis"]').to_html).
to eq %{<a href="#link-emphasis"><em>text</em></a>}
expect(@doc.at_css('a[href="#link-strong"]').to_html).
to eq %{<a href="#link-strong"><strong>text</strong></a>}
expect(@doc.at_css('a[href="#link-code"]').to_html).
to eq %{<a href="#link-code"><code>text</code></a>}
it 'permits br elements' do
expect(doc).to have_selector('br')
end
end
describe 'EmojiFilter' do
it 'parses Emoji' do
expect(@doc).to have_selector('img.emoji', count: 10)
it 'permits hr elements' do
expect(doc).to have_selector('hr')
end
end
describe 'TableOfContentsFilter' do
it 'creates anchors inside header elements' do
expect(@doc).to have_selector('h1 a#gitlab-markdown')
expect(@doc).to have_selector('h2 a#markdown')
expect(@doc).to have_selector('h3 a#autolinkfilter')
it 'permits span elements' do
expect(doc).to have_selector('span:contains("span tag")')
end
end
describe 'AutolinkFilter' do
let(:list) { get_section('autolinkfilter').next_element }
def item(index)
list.at_css("li:nth-child(#{index})")
it 'permits style attribute in th elements' do
aggregate_failures do
expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
end
end
it 'autolinks http://' do
expect(item(1).children.first.name).to eq 'a'
expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/'
it 'permits style attribute in td elements' do
aggregate_failures do
expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
end
end
it 'autolinks https://' do
expect(item(2).children.first.name).to eq 'a'
expect(item(2).children.first['href']).to eq 'https://google.com/'
it 'removes `rel` attribute from links' do
expect(doc).not_to have_selector('a[rel="bookmark"]')
end
it 'autolinks ftp://' do
expect(item(3).children.first.name).to eq 'a'
expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/'
it "removes `href` from `a` elements if it's fishy" do
expect(doc).not_to have_selector('a[href*="javascript"]')
end
end
it 'autolinks smb://' do
expect(item(4).children.first.name).to eq 'a'
expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz'
describe 'Escaping' do
it 'escapes non-tag angle brackets' do
table = doc.css('table').last.at_css('tbody')
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
end
end
it 'autolinks irc://' do
expect(item(5).children.first.name).to eq 'a'
expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git'
end
describe 'Edge Cases' do
it 'allows markup inside link elements' do
aggregate_failures do
expect(doc.at_css('a[href="#link-emphasis"]').to_html).
to eq %{<a href="#link-emphasis"><em>text</em></a>}
it 'autolinks short, invalid URLs' do
expect(item(6).children.first.name).to eq 'a'
expect(item(6).children.first['href']).to eq 'http://localhost:3000'
end
expect(doc.at_css('a[href="#link-strong"]').to_html).
to eq %{<a href="#link-strong"><strong>text</strong></a>}
%w(code a kbd).each do |elem|
it "ignores links inside '#{elem}' element" do
body = get_section('autolinkfilter')
expect(body).not_to have_selector("#{elem} a")
expect(doc.at_css('a[href="#link-code"]').to_html).
to eq %{<a href="#link-code"><code>text</code></a>}
end
end
end
describe 'ExternalLinkFilter' do
let(:links) { get_section('externallinkfilter').next_element }
it 'adds nofollow to external link' do
expect(links.css('a').first.to_html).to match 'nofollow'
link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to match 'nofollow'
end
it 'ignores internal link' do
expect(links.css('a').last.to_html).not_to match 'nofollow'
link = doc.at_css('a:contains("GitLab Root")')
expect(link.attr('rel')).not_to match 'nofollow'
end
end
end
describe 'ReferenceFilter' do
it 'handles references in headers' do
header = @doc.at_css('#reference-filters-eg-1').parent
expect(header.css('a').size).to eq 2
end
it "handles references in Markdown" do
body = get_section('reference-filters-eg-1')
expect(body).to have_selector('em a.gfm-merge_request', count: 1)
end
it 'parses user references' do
body = get_section('userreferencefilter')
expect(body).to have_selector('a.gfm.gfm-project_member', count: 3)
end
it 'parses issue references' do
body = get_section('issuereferencefilter')
expect(body).to have_selector('a.gfm.gfm-issue', count: 2)
end
it 'parses merge request references' do
body = get_section('mergerequestreferencefilter')
expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2)
end
context 'default pipeline' do
before(:all) do
@feat = MarkdownFeature.new
it 'parses snippet references' do
body = get_section('snippetreferencefilter')
expect(body).to have_selector('a.gfm.gfm-snippet', count: 2)
end
# `gfm_with_options` depends on a `@project` variable
@project = @feat.project
it 'parses commit range references' do
body = get_section('commitrangereferencefilter')
expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2)
end
@html = markdown(@feat.raw_markdown)
end
it 'parses commit references' do
body = get_section('commitreferencefilter')
expect(body).to have_selector('a.gfm.gfm-commit', count: 2)
end
it_behaves_like 'all pipelines'
it 'parses label references' do
body = get_section('labelreferencefilter')
expect(body).to have_selector('a.gfm.gfm-label', count: 3)
end
it 'includes RelativeLinkFilter' do
expect(doc).to parse_relative_links
end
describe 'Task Lists' do
it 'generates task lists' do
body = get_section('task-lists')
expect(body).to have_selector('ul.task-list', count: 2)
expect(body).to have_selector('li.task-list-item', count: 7)
expect(body).to have_selector('input[checked]', count: 3)
end
it 'includes EmojiFilter' do
expect(doc).to parse_emoji
end
end
end
# This is a helper class used by the GitLab Markdown feature spec
#
# Because the feature spec only cares about the output of the Markdown, and the
# test setup and teardown and parsing is fairly expensive, we only want to do it
# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
# block, so we fake it by encapsulating all the shared setup in this class.
#
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# reference to the factory-created objects.
class MarkdownFeature
include FactoryGirl::Syntax::Methods
def initialize
DatabaseCleaner.start
end
def teardown
DatabaseCleaner.clean
end
def user
@user ||= create(:user)
end
def group
unless @group
@group = create(:group)
@group.add_user(user, Gitlab::Access::DEVELOPER)
it 'includes TableOfContentsFilter' do
expect(doc).to create_header_links
end
@group
end
# Direct references ----------------------------------------------------------
def project
@project ||= create(:project)
end
def issue
@issue ||= create(:issue, project: project)
end
def merge_request
@merge_request ||= create(:merge_request, :simple, source_project: project)
end
def snippet
@snippet ||= create(:project_snippet, project: project)
end
def commit
@commit ||= project.commit
end
def commit_range
unless @commit_range
commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
it 'includes AutolinkFilter' do
expect(doc).to create_autolinks
end
@commit_range
end
def simple_label
@simple_label ||= create(:label, name: 'gfm', project: project)
end
def label
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
# Cross-references -----------------------------------------------------------
def xproject
unless @xproject
namespace = create(:namespace, name: 'cross-reference')
@xproject = create(:project, namespace: namespace)
@xproject.team << [user, :developer]
it 'includes all reference filters' do
aggregate_failures do
expect(doc).to reference_users
expect(doc).to reference_issues
expect(doc).to reference_merge_requests
expect(doc).to reference_snippets
expect(doc).to reference_commit_ranges
expect(doc).to reference_commits
expect(doc).to reference_labels
end
end
@xproject
end
def xissue
@xissue ||= create(:issue, project: xproject)
end
def xmerge_request
@xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
end
def xsnippet
@xsnippet ||= create(:project_snippet, project: xproject)
end
def xcommit
@xcommit ||= xproject.commit
end
def xcommit_range
unless @xcommit_range
xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
it 'includes TaskListFilter' do
expect(doc).to parse_task_lists
end
end
@xcommit_range
# `markdown` calls these two methods
def current_user
@feat.user
end
def raw_markdown
fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
ERB.new(File.read(fixture)).result(binding)
def user_color_scheme_class
:white
end
end
......@@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try!
- [**text**](#link-strong)
- [`text`](#link-code)
### RelativeLinkFilter
Linking to a file relative to this project's repository should work.
[Relative Link](doc/README.md)
![Relative Image](app/assets/images/touch-icon-ipad.png)
### EmojiFilter
Because life would be :zzz: without Emoji, right? :rocket:
......@@ -123,9 +130,9 @@ These are all plain text that should get turned into links:
But it shouldn't autolink text inside certain tags:
- <code>http://about.gitlab.com/</code>
- <a>http://about.gitlab.com/</a>
- <kbd>http://about.gitlab.com/</kbd>
- <code>http://code.gitlab.com/</code>
- <a>http://a.gitlab.com/</a>
- <kbd>http://kbd.gitlab.com/</kbd>
### ExternalLinkFilter
......
# This is a helper class used by the GitLab Markdown feature spec
#
# Because the feature spec only cares about the output of the Markdown, and the
# test setup and teardown and parsing is fairly expensive, we only want to do it
# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
# block, so we fake it by encapsulating all the shared setup in this class.
#
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# reference to the factory-created objects.
class MarkdownFeature
include FactoryGirl::Syntax::Methods
def user
@user ||= create(:user)
end
def group
unless @group
@group = create(:group)
@group.add_user(user, Gitlab::Access::DEVELOPER)
end
@group
end
# Direct references ----------------------------------------------------------
def project
@project ||= create(:project)
end
def issue
@issue ||= create(:issue, project: project)
end
def merge_request
@merge_request ||= create(:merge_request, :simple, source_project: project)
end
def snippet
@snippet ||= create(:project_snippet, project: project)
end
def commit
@commit ||= project.commit
end
def commit_range
unless @commit_range
commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
end
@commit_range
end
def simple_label
@simple_label ||= create(:label, name: 'gfm', project: project)
end
def label
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
# Cross-references -----------------------------------------------------------
def xproject
unless @xproject
namespace = create(:namespace, name: 'cross-reference')
@xproject = create(:project, namespace: namespace)
@xproject.team << [user, :developer]
end
@xproject
end
def xissue
@xissue ||= create(:issue, project: xproject)
end
def xmerge_request
@xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
end
def xsnippet
@xsnippet ||= create(:project_snippet, project: xproject)
end
def xcommit
@xcommit ||= xproject.commit
end
def xcommit_range
unless @xcommit_range
xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end
@xcommit_range
end
def raw_markdown
fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
ERB.new(File.read(fixture)).result(binding)
end
end
# MarkdownMatchers
#
# Custom matchers for our custom HTML::Pipeline filters. These are used to test
# that specific filters are or are not used by our defined pipelines.
#
# Must be included manually.
module MarkdownMatchers
extend RSpec::Matchers::DSL
include Capybara::Node::Matchers
# RelativeLinkFilter
matcher :parse_relative_links do
set_default_markdown_messages
match do |actual|
link = actual.at_css('a:contains("Relative Link")')
image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md')
expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
end
end
# EmojiFilter
matcher :parse_emoji do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('img.emoji', count: 10)
end
end
# TableOfContentsFilter
matcher :create_header_links do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('h1 a#gitlab-markdown')
expect(actual).to have_selector('h2 a#markdown')
expect(actual).to have_selector('h3 a#autolinkfilter')
end
end
# AutolinkFilter
matcher :create_autolinks do
def have_autolink(link)
have_link(link, href: link)
end
set_default_markdown_messages
match do |actual|
expect(actual).to have_autolink('http://about.gitlab.com/')
expect(actual).to have_autolink('https://google.com/')
expect(actual).to have_autolink('ftp://ftp.us.debian.org/debian/')
expect(actual).to have_autolink('smb://foo/bar/baz')
expect(actual).to have_autolink('irc://irc.freenode.net/git')
expect(actual).to have_autolink('http://localhost:3000')
%w(code a kbd).each do |elem|
expect(body).not_to have_selector("#{elem} a")
end
end
end
# UserReferenceFilter
matcher :reference_users do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3)
end
end
# IssueReferenceFilter
matcher :reference_issues do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-issue', count: 3)
end
end
# MergeRequestReferenceFilter
matcher :reference_merge_requests do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3)
expect(actual).to have_selector('em a.gfm-merge_request')
end
end
# SnippetReferenceFilter
matcher :reference_snippets do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2)
end
end
# CommitRangeReferenceFilter
matcher :reference_commit_ranges do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2)
end
end
# CommitReferenceFilter
matcher :reference_commits do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-commit', count: 2)
end
end
# LabelReferenceFilter
matcher :reference_labels do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-label', count: 3)
end
end
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('ul.task-list', count: 2)
expect(actual).to have_selector('li.task-list-item', count: 7)
expect(actual).to have_selector('input[checked]', count: 3)
end
end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
# setting the failure messages for these matchers
module RSpec::Matchers::DSL::Macros
def set_default_markdown_messages
failure_message do
# expected to parse emoji, but didn't
"expected to #{description}, but didn't"
end
failure_message_when_negated do
# expected not to parse task lists, but did
"expected not to #{description}, but did"
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment