Commit 132cc71d authored by Stan Hu's avatar Stan Hu

Merge branch...

Merge branch '338268-refactor-golden-master-spec-approach-for-testing-gitlab-flavored-markdown-support-in-the' into 'master'

Refactor Golden Master spec approach for testing GFM support

See merge request gitlab-org/gitlab!68671
parents c5cb8ce8 2518ceeb
# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation.
---
- name: attachment_link_for_group_wiki
api_context: group_wiki
substitutions:
# NOTE: We don't care about verifying specific attribute values here, that should be the
# responsibility of unit tests. These tests are about the structure of the HTML.
path_attribute_id_substitution:
- regex: '(group|project)(\d+)'
replacement: '\1ID'
markdown: |-
[test-file](test-file.zip)
html: |-
<p data-sourcepos="1:1-1:26" dir="auto"><a href="/groups/group2/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a></p>
import path from 'path';
import {
createSharedExamples,
loadMarkdownApiExamples,
} from 'jest/content_editor/markdown_processing_spec_helper';
jest.mock('~/emoji');
// See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
describe('EE markdown processing in ContentEditor', () => {
// Ensure we generate same markdown that was provided to Markdown API.
const markdownYamlPath = path.join(
__dirname,
'..',
'..',
'fixtures',
'markdown',
'markdown_golden_master_examples.yml',
);
// eslint-disable-next-line jest/valid-describe
describe.each(loadMarkdownApiExamples(markdownYamlPath))('%s', createSharedExamples);
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include WikiHelpers
include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:group_wiki) { create(:group_wiki, user: user) }
let(:group_wiki_page) { create(:wiki_page, wiki: group_wiki) }
let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
before(:all) do
group.add_owner(user)
end
before do
stub_group_wikis(true)
sign_in(user)
end
markdown_examples = begin
yaml_file_path = File.expand_path('api_markdown.yml', __dir__)
yaml = File.read(yaml_file_path)
YAML.safe_load(yaml, symbolize_names: true)
end
markdown_examples.each do |markdown_example|
context = markdown_example.fetch(:context, '')
name = markdown_example.fetch(:name)
context "for #{name}#{!context.empty? ? " (context: #{context})" : ''}" do
let(:markdown) { markdown_example.fetch(:markdown) }
name = "#{context}_#{name}" unless context.empty?
it "api/markdown/#{name}.json" do
api_url = case context
when 'group_wiki'
"/groups/#{group.full_path}/-/wikis/#{group_wiki_page.slug}/preview_markdown"
else
api "/markdown"
end
post api_url, params: { text: markdown, gfm: true }
expect(response).to be_successful
end
end
end
end
# This data file drives the specs in
# ee/spec/frontend/fixtures/api_markdown.rb and
---
- name: attachment_link
context: group_wiki
markdown: '[test-file](test-file.zip)'
# frozen_string_literal: true
require 'spec_helper'
# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
RSpec.describe API::Markdown, 'EE Golden Master' do
include WikiHelpers
let_it_be(:user) { create(:user, username: 'gfm_ee_user') }
let_it_be(:group_wiki) { create(:group_wiki, user: user) }
let_it_be(:group_wiki_page) { create(:wiki_page, wiki: group_wiki) }
before do
stub_group_wikis(true)
sign_in(user)
end
markdown_yml_file_path = File.expand_path('../../fixtures/markdown/markdown_golden_master_examples.yml', __dir__)
include_examples 'API::Markdown Golden Master shared context', markdown_yml_file_path do
extend ::Gitlab::Utils::Override
override :supported_api_contexts
def supported_api_contexts
super + %w(group_wiki)
end
override :get_url_for_api_context
def get_url_for_api_context(api_context)
case api_context
when 'group_wiki'
"/groups/#{group.full_path}/-/wikis/#{group_wiki_page.slug}/preview_markdown"
else
super
end
end
end
end
This diff is collapsed.
import fs from 'fs';
import path from 'path';
import jsYaml from 'js-yaml';
// eslint-disable-next-line import/no-deprecated
import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
// eslint-disable-next-line import/no-deprecated
const fixture = getJSONFixture(fixturePathPrefix);
return fixture.body || fixture.html;
};
export const loadMarkdownApiExamples = () => {
const apiMarkdownYamlPath = path.join(__dirname, '..', 'fixtures', 'api_markdown.yml');
const apiMarkdownYamlText = fs.readFileSync(apiMarkdownYamlPath);
const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
};
export const loadMarkdownApiExample = (testName) => {
return loadMarkdownApiExamples().find(([name, context]) => {
return (context ? `${context}_${name}` : name) === testName;
})[2];
};
import { createContentEditor } from '~/content_editor'; import path from 'path';
import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples'; import { createSharedExamples, loadMarkdownApiExamples } from './markdown_processing_spec_helper';
jest.mock('~/emoji'); jest.mock('~/emoji');
describe('markdown processing', () => { // See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
describe('markdown processing in ContentEditor', () => {
// Ensure we generate same markdown that was provided to Markdown API. // Ensure we generate same markdown that was provided to Markdown API.
it.each(loadMarkdownApiExamples())( const markdownYamlPath = path.join(
'correctly handles %s (context: %s)', __dirname,
async (name, context, markdown) => { '..',
const testName = context ? `${context}_${name}` : name; '..',
const contentEditor = createContentEditor({ 'fixtures',
renderMarkdown: () => loadMarkdownApiResult(testName), 'markdown',
}); 'markdown_golden_master_examples.yml',
await contentEditor.setSerializedContent(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown);
},
); );
// eslint-disable-next-line jest/valid-describe
describe.each(loadMarkdownApiExamples(markdownYamlPath))('%s', createSharedExamples);
}); });
import fs from 'fs';
import jsYaml from 'js-yaml';
import { memoize } from 'lodash';
import { createContentEditor } from '~/content_editor';
import { setTestTimeoutOnce } from 'helpers/timeout';
const getFocusedMarkdownExamples = memoize(
() => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
);
const includeExample = ({ name }) => {
const focusedMarkdownExamples = getFocusedMarkdownExamples();
if (!focusedMarkdownExamples.length) {
return true;
}
return focusedMarkdownExamples.includes(name);
};
const getPendingReason = (pendingStringOrObject) => {
if (!pendingStringOrObject) {
return null;
}
if (typeof pendingStringOrObject === 'string') {
return pendingStringOrObject;
}
if (pendingStringOrObject.frontend) {
return pendingStringOrObject.frontend;
}
return null;
};
// eslint-disable-next-line jest/no-export
export const loadMarkdownApiExamples = (markdownYamlPath) => {
const apiMarkdownYamlText = fs.readFileSync(markdownYamlPath);
const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
return apiMarkdownExampleObjects
.filter(includeExample)
.map(({ name, pending, markdown, html }) => [
name,
{ pendingReason: getPendingReason(pending), markdown, html },
]);
};
const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
const contentEditor = createContentEditor({
// Overwrite renderMarkdown to always return this specific html
renderMarkdown: () => html,
});
await contentEditor.setSerializedContent(markdown);
// This serializes the ContentEditor document, which was based on the HTML, to markdown
const serializedContent = contentEditor.getSerializedContent();
// Assert that the markdown we ended up with after sending it through all the ContentEditor
// plumbing matches the original markdown from the YAML.
expect(serializedContent).toBe(markdown);
};
// eslint-disable-next-line jest/no-export
export const createSharedExamples = (name, { pendingReason, ...example }) => {
const exampleName = 'correctly serializes HTML to markdown';
if (pendingReason) {
it.todo(`${exampleName}: ${pendingReason}`);
} else {
it(exampleName, async () => {
if (name === 'frontmatter_toml') {
setTestTimeoutOnce(2000);
}
await testSerializesHtmlToMarkdownForElement(example);
});
}
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include WikiHelpers
include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user, username: 'gitlab') }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, :repository, group: group) }
let_it_be(:label) { create(:label, project: project, title: 'bug') }
let_it_be(:milestone) { create(:milestone, project: project, title: '1.1') }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
before(:all) do
group.add_owner(user)
project.add_maintainer(user)
end
before do
sign_in(user)
end
markdown_examples = begin
yaml_file_path = File.expand_path('api_markdown.yml', __dir__)
yaml = File.read(yaml_file_path)
YAML.safe_load(yaml, symbolize_names: true)
end
markdown_examples.each do |markdown_example|
context = markdown_example.fetch(:context, '')
name = markdown_example.fetch(:name)
context "for #{name}#{!context.empty? ? " (context: #{context})" : ''}" do
let(:markdown) { markdown_example.fetch(:markdown) }
name = "#{context}_#{name}" unless context.empty?
it "api/markdown/#{name}.json" do
api_url = case context
when 'project'
"/#{project.full_path}/preview_markdown"
when 'group'
"/groups/#{group.full_path}/preview_markdown"
when 'project_wiki'
"/#{project.full_path}/-/wikis/#{project_wiki_page.slug}/preview_markdown"
else
api "/markdown"
end
post api_url, params: { text: markdown, gfm: true }
expect(response).to be_successful
end
end
end
end
# This data file drives the specs in
# spec/frontend/fixtures/api_markdown.rb and
# spec/frontend/content_editor/extensions/markdown_processing_spec.js
---
- name: attachment_image
context: group
markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
- name: attachment_image
context: project
markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
- name: attachment_image
context: project_wiki
markdown: '![test-file](test-file.png)'
- name: attachment_link
context: group
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
- name: attachment_link
context: project
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
- name: attachment_link
context: project_wiki
markdown: '[test-file](test-file.zip)'
- name: audio
markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
- name: audio_and_video_in_lists
markdown: |-
* ![Sample Audio](https://gitlab.com/1.mp3)
* ![Sample Video](https://gitlab.com/2.mp4)
1. ![Sample Video](https://gitlab.com/1.mp4)
2. ![Sample Audio](https://gitlab.com/2.mp3)
* [x] ![Sample Audio](https://gitlab.com/1.mp3)
* [x] ![Sample Audio](https://gitlab.com/2.mp3)
* [x] ![Sample Video](https://gitlab.com/3.mp4)
- name: blockquote
markdown: |-
> This is a blockquote
>
> This is another one
- name: bold
markdown: '**bold**'
- name: bullet_list_style_1
markdown: |-
* list item 1
* list item 2
* embedded list item 3
- name: bullet_list_style_2
markdown: |-
- list item 1
- list item 2
* embedded list item 3
- name: bullet_list_style_3
markdown: |-
+ list item 1
+ list item 2
- embedded list item 3
- name: code_block
markdown: |-
```javascript
console.log('hello world')
```
- name: color_chips
markdown: |-
- `#F00`
- `#F00A`
- `#FF0000`
- `#FF0000AA`
- `RGB(0,255,0)`
- `RGB(0%,100%,0%)`
- `RGBA(0,255,0,0.3)`
- `HSL(540,70%,50%)`
- `HSLA(540,70%,50%,0.3)`
- name: description_list
markdown: |-
<dl>
<dt>Frog</dt>
<dd>Wet green thing</dd>
<dt>Rabbit</dt>
<dd>Warm fluffy thing</dd>
<dt>Punt</dt>
<dd>Kick a ball</dd>
<dd>Take a bet</dd>
<dt>Color</dt>
<dt>Colour</dt>
<dd>
Any hue except _white_ or **black**
</dd>
</dl>
- name: details
markdown: |-
<details>
<summary>Apply this patch</summary>
```diff
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 8433efaf00c..69b12c59d46 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -33,6 +33,13 @@
* <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
* C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
* The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>.The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
+- name: details
+ markdown: |-
+ <details>
+ <summary>Apply this patch</summary>
+
+ 🐶 much meta, 🐶 many patch
+ 🐶 such diff, 🐶 very meme
+ 🐶 wow!
+ </details>
- name: link
markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link
```
</details>
- name: div
markdown: |-
<div>plain text</div>
<div>
just a plain ol' div, not much to _expect_!
</div>
- name: emoji
markdown: ':sparkles: :heart: :100:'
- name: emphasis
markdown: '_emphasized text_'
- name: figure
markdown: |-
<figure>
![Elephant at sunset](elephant-sunset.jpg)
<figcaption>An elephant at sunset</figcaption>
</figure>
<figure>
![A crocodile wearing crocs](croc-crocs.jpg)
<figcaption>
A crocodile wearing _crocs_!
</figcaption>
</figure>
- name: frontmatter_json
markdown: |-
;;;
{
"title": "Page title"
}
;;;
- name: frontmatter_toml
markdown: |-
+++
title = "Page title"
+++
- name: frontmatter_yaml
markdown: |-
---
title: Page title
---
- name: hard_break
markdown: |-
This is a line after a\
hard break
- name: headings
markdown: |-
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
- name: horizontal_rule
markdown: '---'
- name: html_marks
markdown: |-
* Content editor is ~~great~~<ins>amazing</ins>.
* If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
* The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
* <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
* <dfn>HTML</dfn> is the standard markup language for creating web pages.
* Do not forget to buy <mark>milk</mark> today.
* This is a paragraph and <small>smaller text goes here</small>.
* The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
* Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
* WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
* The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
* The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
* <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
* C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
* The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
- name: image
markdown: '![alt text](https://gitlab.com/logo.png)'
- name: inline_code
markdown: '`code`'
- name: inline_diff
markdown: |-
* {-deleted-}
* {+added+}
- name: link
markdown: '[GitLab](https://gitlab.com)'
- name: math
markdown: |-
This math is inline $`a^2+b^2=c^2`$.
This is on a separate line:
```math
a^2+b^2=c^2
```
- name: ordered_list
markdown: |-
1. list item 1
2. list item 2
3. list item 3
- name: ordered_list_with_start_order
markdown: |-
134. list item 1
135. list item 2
136. list item 3
- name: ordered_task_list
markdown: |-
1. [x] hello
2. [x] world
3. [ ] example
1. [ ] of nested
1. [x] task list
2. [ ] items
- name: ordered_task_list_with_order
markdown: |-
4893. [x] hello
4894. [x] world
4895. [ ] example
- name: reference
context: project_wiki
markdown: |-
Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
- name: strike
markdown: '~~del~~'
- name: table
markdown: |-
| header | header |
|--------|--------|
| `code` | cell with **bold** |
| ~~strike~~ | cell with _italic_ |
# content after table
- name: table_of_contents
markdown: |-
[[_TOC_]]
# Lorem
Well, that's just like... your opinion.. man.
## Ipsum
### Dolar
# Sit amit
### I don't know
- name: task_list
markdown: |-
* [x] hello
* [x] world
* [ ] example
* [ ] of nested
* [x] task list
* [ ] items
- name: thematic_break
markdown: |-
---
- name: video
markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
- name: word_break
markdown: Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
# frozen_string_literal: true
require 'spec_helper'
# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
RSpec.describe API::Markdown, 'Golden Master' do
markdown_yml_file_path = File.expand_path('../../fixtures/markdown/markdown_golden_master_examples.yml', __dir__)
include_context 'API::Markdown Golden Master shared context', markdown_yml_file_path
end
...@@ -450,6 +450,13 @@ RSpec.configure do |config| ...@@ -450,6 +450,13 @@ RSpec.configure do |config|
$stdout = StringIO.new $stdout = StringIO.new
end end
# Makes diffs show entire non-truncated values.
config.before(:each, unlimited_max_formatted_output_length: true) do |_example|
config.expect_with :rspec do |c|
c.max_formatted_output_length = nil
end
end
config.after(:each, :silence_stdout) do config.after(:each, :silence_stdout) do
$stdout = STDOUT $stdout = STDOUT
end end
......
# frozen_string_literal: true
require 'spec_helper'
# See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_yml_file_path|
include ApiHelpers
include WikiHelpers
let_it_be(:user) { create(:user, username: 'gfm_user') }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, :repository, group: group) }
let_it_be(:label) { create(:label, project: project, title: 'bug') }
let_it_be(:milestone) { create(:milestone, project: project, title: '1.1') }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) }
let_it_be(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
before(:all) do
group.add_owner(user)
project.add_maintainer(user)
end
before do
sign_in(user)
end
markdown_examples = begin
yaml = File.read(markdown_yml_file_path)
YAML.safe_load(yaml, symbolize_names: true, aliases: true)
end
it "examples must be unique and alphabetized by name", :unlimited_max_formatted_output_length do
names = markdown_examples.map { |example| example[:name] }
expect(names).to eq(names.sort.uniq)
end
if focused_markdown_examples_string = ENV['FOCUSED_MARKDOWN_EXAMPLES']
focused_markdown_examples = focused_markdown_examples_string.split(',').map(&:strip) || []
markdown_examples.reject! {|markdown_example| !focused_markdown_examples.include?(markdown_example.fetch(:name)) }
end
markdown_examples.each do |markdown_example|
name = markdown_example.fetch(:name)
api_context = markdown_example[:api_context]
if api_context && !name.end_with?("_for_#{api_context}")
raise "Name must have suffix of '_for_#{api_context}' to the api_context"
end
context "for #{name}#{api_context ? " (api_context: #{api_context})" : ''}" do
let(:pending_reason) do
pending_value = markdown_example.fetch(:pending, nil)
get_pending_reason(pending_value)
end
let(:example_markdown) { markdown_example.fetch(:markdown) }
let(:example_html) { markdown_example.fetch(:html) }
let(:substitutions) { markdown_example.fetch(:substitutions, {}) }
it "verifies conversion of GFM to HTML", :unlimited_max_formatted_output_length do
pending pending_reason if pending_reason
normalized_example_html = normalize_html(example_html, substitutions)
api_url = get_url_for_api_context(api_context)
post api_url, params: { text: example_markdown, gfm: true }
expect(response).to be_successful
response_body = Gitlab::Json.parse(response.body)
# Some requests have the HTML in the `html` key, others in the `body` key.
response_html = response_body['body'] ? response_body.fetch('body') : response_body.fetch('html')
normalized_response_html = normalize_html(response_html, substitutions)
expect(normalized_response_html).to eq(normalized_example_html)
end
def get_pending_reason(pending_value)
return false unless pending_value
return pending_value if pending_value.is_a?(String)
pending_value[:backend] || false
end
def normalize_html(html, substitutions)
normalized_html = html.dup
# Note: having the top level `substitutions` data structure be a hash of arrays
# allows us to compose multiple substitutions via YAML anchors (YAML anchors
# pointing to arrays can't be combined)
substitutions.each_value do |substitution_entry|
substitution_entry.each do |substitution|
regex = substitution.fetch(:regex)
replacement = substitution.fetch(:replacement)
normalized_html.gsub!(%r{#{regex}}, replacement)
end
end
normalized_html
end
end
end
def supported_api_contexts
%w(project group project_wiki)
end
def get_url_for_api_context(api_context)
case api_context
when 'project'
"/#{project.full_path}/preview_markdown"
when 'group'
"/groups/#{group.full_path}/preview_markdown"
when 'project_wiki'
"/#{project.full_path}/-/wikis/#{project_wiki_page.slug}/preview_markdown"
when nil
api "/markdown"
else
raise "Error: 'context' extension was '#{api_context}'. It must be one of: #{supported_api_contexts.join(',')}"
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