diff --git a/client/src/components/Draftail/decorators/Link.test.js b/client/src/components/Draftail/decorators/Link.test.js index 56c5b4f3b7..5b2a492d35 100644 --- a/client/src/components/Draftail/decorators/Link.test.js +++ b/client/src/components/Draftail/decorators/Link.test.js @@ -5,6 +5,7 @@ import { convertFromHTML, ContentState, EditorState, + convertToRaw, } from 'draft-js'; import Link, { getLinkAttributes, getValidLinkURL, onPasteLink } from './Link'; @@ -136,6 +137,7 @@ describe.each` describe('onPasteLink', () => { let editorState; let setEditorState; + let testOnPasteOutput; beforeEach(() => { const { contentBlocks } = convertFromHTML('
hello
'); @@ -143,6 +145,14 @@ describe('onPasteLink', () => { editorState = EditorState.createWithContent(contentState); setEditorState = jest.fn((state) => state); + + testOnPasteOutput = (text, html) => { + expect(onPasteLink(text, html, editorState, { setEditorState })).toBe( + 'handled', + ); + const content = setEditorState.mock.calls[0][0].getCurrentContent(); + return convertToRaw(content); + }; }); it('discards invalid URLs', () => { @@ -152,7 +162,7 @@ describe('onPasteLink', () => { expect(setEditorState).not.toHaveBeenCalled(); }); - it('creates link onto selected text', () => { + it('single link onto selected text', () => { const selection = editorState.getSelection().merge({ focusOffset: 4, }); @@ -171,7 +181,7 @@ describe('onPasteLink', () => { ).toBe('https://example.com/selected'); }); - it('creates link with paste as link text when collapsed', () => { + it('single link without selection', () => { expect( onPasteLink('https://example.com/collapsed', null, editorState, { setEditorState, @@ -186,4 +196,84 @@ describe('onPasteLink', () => { content.getEntity(content.getLastCreatedEntityKey()).getData().url, ).toBe('https://example.com/collapsed'); }); + + it('multiple plain-text links and emails', () => { + const input = + 'http://a.co/ ftp://b.co/ test@example.com https://c.co/ ftps://d.co'; + const raw = testOnPasteOutput(input, null); + expect(raw.blocks[0]).toMatchObject({ + text: `${input}hello`, + entityRanges: [ + { offset: 0, length: 12, key: 0 }, + { offset: 13, length: 11, key: 1 }, + { offset: 25, length: 16, key: 2 }, + { offset: 42, length: 13, key: 3 }, + { offset: 56, length: 11, key: 4 }, + ], + }); + expect(raw.entityMap).toMatchObject({ + 0: { + type: 'LINK', + mutability: 'MUTABLE', + data: { url: 'http://a.co/' }, + }, + 1: { data: { url: 'ftp://b.co/' } }, + 2: { data: { url: 'mailto:test@example.com' } }, + 3: { data: { url: 'https://c.co/' } }, + 4: { data: { url: 'ftps://d.co' } }, + }); + }); + + it('multiple URLs and emails within HTML', () => { + const input = ` +start
+l1 http://a.co/
+l2a test@example.com l2b
+https://c.co/ l3
+end
+ `; + const raw = testOnPasteOutput(input.replace(/<[^>]+/g), input); + expect(raw.blocks).toMatchObject([ + { text: 'start', entityRanges: [] }, + { + text: 'l1 http://a.co/', + entityRanges: [{ offset: 3, length: 12, key: 0 }], + }, + { + text: 'l2a test@example.com l2b', + entityRanges: [{ offset: 4, length: 16, key: 1 }], + }, + { + text: 'https://c.co/ l3', + entityRanges: [{ offset: 0, length: 13, key: 2 }], + }, + { text: 'endhello', entityRanges: [] }, + ]); + expect(raw.entityMap).toMatchObject({ + 0: { data: { url: 'http://a.co/' } }, + 1: { data: { url: 'mailto:test@example.com' } }, + 2: { data: { url: 'https://c.co/' } }, + }); + }); + + it('preserves existing rich text', () => { + const input = ` +http://a.co/
+ + `; + const raw = testOnPasteOutput(input.replace(/<[^>]+/g), input); + expect(raw.blocks).toMatchObject([ + { type: 'header-two', text: 'Heading', entityRanges: [] }, + { + text: 'http://a.co/', + entityRanges: [{ offset: 0, length: 12, key: 0 }], + }, + { text: 'linkhello', entityRanges: [{ offset: 0, length: 4, key: 1 }] }, + ]); + expect(raw.entityMap).toMatchObject({ + 0: { data: { url: 'http://a.co/' } }, + 1: { data: { url: 'http://test.co/' } }, + }); + }); });