Commit 0790bb72 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'original-url-tests-markdown' into 'master'

Add support for data-canonical-src in content editor links + add tests

See merge request gitlab-org/gitlab!64194
parents b324db38 3a0d395c
...@@ -43,14 +43,22 @@ export default { ...@@ -43,14 +43,22 @@ export default {
}, },
mounted() { mounted() {
this.tiptapEditor.on('selectionUpdate', ({ editor }) => { this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
const { href } = editor.getAttributes(linkContentType); const { 'data-canonical-src': canonicalSrc, href } = editor.getAttributes(linkContentType);
this.linkHref = href; this.linkHref = canonicalSrc || href;
}); });
}, },
methods: { methods: {
updateLink() { updateLink() {
this.tiptapEditor.chain().focus().unsetLink().setLink({ href: this.linkHref }).run(); this.tiptapEditor
.chain()
.focus()
.unsetLink()
.setLink({
href: this.linkHref,
'data-canonical-src': this.linkHref,
})
.run();
this.$emit('execute', { contentType: linkContentType }); this.$emit('execute', { contentType: linkContentType });
}, },
......
import { markInputRule } from '@tiptap/core'; import { markInputRule } from '@tiptap/core';
import { Link } from '@tiptap/extension-link'; import { Link } from '@tiptap/extension-link';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm; export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim; export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
const extractHrefFromMatch = (match) => { const extractHrefFromMatch = (match) => {
...@@ -29,8 +27,37 @@ export const tiptapExtension = Link.extend({ ...@@ -29,8 +27,37 @@ export const tiptapExtension = Link.extend({
markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch), markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch),
]; ];
}, },
addAttributes() {
return {
...this.parent?.(),
href: {
default: null,
parseHTML: (element) => {
return {
href: element.getAttribute('href'),
};
},
},
'data-canonical-src': {
default: null,
parseHTML: (element) => {
return {
href: element.dataset.canonicalSrc,
};
},
},
};
},
}).configure({ }).configure({
openOnClick: false, openOnClick: false,
}); });
export const serializer = defaultMarkdownSerializer.marks.link; export const serializer = {
open() {
return '[';
},
close(state, mark) {
const href = mark.attrs['data-canonical-src'] || mark.attrs.href;
return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
};
import { GlDropdown, GlDropdownDivider, GlFormInputGroup, GlButton } from '@gitlab/ui'; import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
import { tiptapExtension as Link } from '~/content_editor/extensions/link'; import { tiptapExtension as Link } from '~/content_editor/extensions/link';
...@@ -16,9 +16,6 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -16,9 +16,6 @@ describe('content_editor/components/toolbar_link_button', () => {
propsData: { propsData: {
tiptapEditor: editor, tiptapEditor: editor,
}, },
stubs: {
GlFormInputGroup,
},
}); });
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
...@@ -45,9 +42,8 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -45,9 +42,8 @@ describe('content_editor/components/toolbar_link_button', () => {
}); });
describe('when there is an active link', () => { describe('when there is an active link', () => {
beforeEach(() => { beforeEach(async () => {
jest.spyOn(editor, 'isActive'); jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
editor.isActive.mockReturnValueOnce(true);
buildWrapper(); buildWrapper();
}); });
...@@ -78,9 +74,35 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -78,9 +74,35 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(commands.focus).toHaveBeenCalled(); expect(commands.focus).toHaveBeenCalled();
expect(commands.unsetLink).toHaveBeenCalled(); expect(commands.unsetLink).toHaveBeenCalled();
expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' }); expect(commands.setLink).toHaveBeenCalledWith({
href: 'https://example',
'data-canonical-src': 'https://example',
});
expect(commands.run).toHaveBeenCalled(); expect(commands.run).toHaveBeenCalled();
}); });
describe('on selection update', () => {
it('updates link input box with canonical-src if present', async () => {
jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
'data-canonical-src': 'uploads/my-file.zip',
href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
});
await editor.emit('selectionUpdate', { editor });
expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip');
});
it('updates link input box with link href otherwise', async () => {
jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
href: 'https://gitlab.com',
});
await editor.emit('selectionUpdate', { editor });
expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
});
});
}); });
describe('when there is not an active link', () => { describe('when there is not an active link', () => {
...@@ -106,7 +128,10 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -106,7 +128,10 @@ describe('content_editor/components/toolbar_link_button', () => {
await findApplyLinkButton().trigger('click'); await findApplyLinkButton().trigger('click');
expect(commands.focus).toHaveBeenCalled(); expect(commands.focus).toHaveBeenCalled();
expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' }); expect(commands.setLink).toHaveBeenCalledWith({
href: 'https://example',
'data-canonical-src': 'https://example',
});
expect(commands.run).toHaveBeenCalled(); expect(commands.run).toHaveBeenCalled();
}); });
}); });
......
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import jsYaml from 'js-yaml'; import jsYaml from 'js-yaml';
import { toArray } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => { export const loadMarkdownApiResult = (testName) => {
...@@ -15,5 +14,5 @@ export const loadMarkdownApiExamples = () => { ...@@ -15,5 +14,5 @@ export const loadMarkdownApiExamples = () => {
const apiMarkdownYamlText = fs.readFileSync(apiMarkdownYamlPath); const apiMarkdownYamlText = fs.readFileSync(apiMarkdownYamlPath);
const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText); const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
return apiMarkdownExampleObjects.map((example) => toArray(example)); return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
}; };
...@@ -3,11 +3,15 @@ import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_proce ...@@ -3,11 +3,15 @@ import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_proce
describe('markdown processing', () => { describe('markdown processing', () => {
// 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())('correctly handles %s', async (testName, markdown) => { it.each(loadMarkdownApiExamples())(
const { html } = loadMarkdownApiResult(testName); 'correctly handles %s (context: %s)',
const contentEditor = createContentEditor({ renderMarkdown: () => html }); async (name, context, markdown) => {
await contentEditor.setSerializedContent(markdown); const testName = context ? `${context}_${name}` : name;
const { html, body } = loadMarkdownApiResult(testName);
const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
await contentEditor.setSerializedContent(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown); expect(contentEditor.getSerializedContent()).toBe(markdown);
}); },
);
}); });
...@@ -4,12 +4,32 @@ require 'spec_helper' ...@@ -4,12 +4,32 @@ require 'spec_helper'
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers include ApiHelpers
include WikiHelpers
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, :repository, group: group) }
let_it_be(:group_wiki) { create(:group_wiki, user: user) }
let_it_be(:project_wiki) { create(:project_wiki, user: user) }
let(:group_wiki_page) { create(:wiki_page, wiki: group_wiki) }
let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
fixture_subdir = 'api/markdown' fixture_subdir = 'api/markdown'
before(:all) do before(:all) do
clean_frontend_fixtures(fixture_subdir) clean_frontend_fixtures(fixture_subdir)
group.add_owner(user)
project.add_maintainer(user)
end
before do
stub_group_wikis(true)
sign_in(user)
end end
markdown_examples = begin markdown_examples = begin
...@@ -19,14 +39,29 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do ...@@ -19,14 +39,29 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
end end
markdown_examples.each do |markdown_example| markdown_examples.each do |markdown_example|
context = markdown_example.fetch(:context, '')
name = markdown_example.fetch(:name) name = markdown_example.fetch(:name)
context "for #{name}" do context "for #{name}#{!context.empty? ? " (context: #{context})" : ''}" do
let(:markdown) { markdown_example.fetch(:markdown) } let(:markdown) { markdown_example.fetch(:markdown) }
name = "#{context}_#{name}" unless context.empty?
it "#{fixture_subdir}/#{name}.json" do it "#{fixture_subdir}/#{name}.json" do
post api("/markdown"), params: { text: markdown, gfm: true } 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"
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 expect(response).to be_successful
end end
end end
......
...@@ -14,6 +14,18 @@ ...@@ -14,6 +14,18 @@
markdown: '---' markdown: '---'
- name: link - name: link
markdown: '[GitLab](https://gitlab.com)' markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link
context: project_wiki
markdown: '[test-file](test-file.zip)'
- name: attachment_link
context: project
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
- name: attachment_link
context: group_wiki
markdown: '[test-file](test-file.zip)'
- name: attachment_link
context: group
markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
- name: code_block - name: code_block
markdown: |- markdown: |-
```javascript ```javascript
......
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