import CopyPaste from '~/content_editor/extensions/copy_paste'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import Loading, { findAllLoaders } from '~/content_editor/extensions/loading'; import Diagram from '~/content_editor/extensions/diagram'; import Frontmatter from '~/content_editor/extensions/frontmatter'; import Selection from '~/content_editor/extensions/selection'; import Heading from '~/content_editor/extensions/heading'; import Bold from '~/content_editor/extensions/bold'; import BulletList from '~/content_editor/extensions/bullet_list'; import ListItem from '~/content_editor/extensions/list_item'; import Italic from '~/content_editor/extensions/italic'; import Table from '~/content_editor/extensions/table'; import TableCell from '~/content_editor/extensions/table_cell'; import TableRow from '~/content_editor/extensions/table_row'; import TableHeader from '~/content_editor/extensions/table_header'; import { VARIANT_DANGER } from '~/alert'; import eventHubFactory from '~/helpers/event_hub_factory'; import { ALERT_EVENT } from '~/content_editor/constants'; import waitForPromises from 'helpers/wait_for_promises'; import MarkdownSerializer from '~/content_editor/services/markdown_serializer'; import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils'; const PARAGRAPH_HTML = '
Some text with bold and italic text.
'; describe('content_editor/extensions/copy_paste', () => { let tiptapEditor; let doc; let p; let bold; let italic; let heading; let codeBlock; let bulletList; let listItem; let renderMarkdown; let resolveRenderMarkdownPromiseAndWait; let eventHub; const defaultData = { 'text/plain': '**bold text**' }; beforeEach(() => { eventHub = eventHubFactory(); renderMarkdown = jest.fn().mockImplementation( () => new Promise((resolve) => { resolveRenderMarkdownPromiseAndWait = (data) => waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) }); }), ); jest.spyOn(eventHub, '$emit'); tiptapEditor = createTestEditor({ extensions: [ Bold, Italic, Loading, Selection, CodeBlockHighlight, Diagram, Frontmatter, Heading, BulletList, ListItem, Table, TableCell, TableRow, TableHeader, CopyPaste.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }), ], }); ({ builders: { doc, p, bold, italic, heading, codeBlock, bulletList, listItem }, } = createDocBuilder({ tiptapEditor, names: { bold: { markType: Bold.name }, italic: { markType: Italic.name }, heading: { nodeType: Heading.name }, bulletList: { nodeType: BulletList.name }, listItem: { nodeType: ListItem.name }, codeBlock: { nodeType: CodeBlockHighlight.name }, diagram: { nodeType: Diagram.name }, frontmatter: { nodeType: Frontmatter.name }, }, })); }); const buildClipboardEvent = ({ eventName = 'paste', data = {}, types = ['text/plain'] } = {}) => { return Object.assign(new Event(eventName), { clipboardData: { types, getData: jest.fn((type) => data[type] || defaultData[type]), setData: jest.fn(), clearData: jest.fn(), }, }); }; const triggerPasteEventHandler = (event) => { return new Promise((resolve) => { tiptapEditor.view.someProp('handlePaste', (eventHandler) => { resolve(eventHandler(tiptapEditor.view, event)); }); }); }; it.each` types | data | formatDesc ${['text/plain']} | ${{}} | ${'plain text'} ${['text/plain', 'text/html']} | ${{}} | ${'html format'} ${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "markdown" }' }} | ${'vscode markdown'} ${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "ruby" }' }} | ${'vscode snippet'} `('handles $formatDesc', async ({ types, data }) => { expect(await triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(true); }); describe.each` eventName | expectedDoc ${'cut'} | ${() => doc(p())} ${'copy'} | ${() => doc(p('Some text with ', bold('bold'), ' and ', italic('italic'), ' text.'))} `('when $eventName event is triggered', ({ eventName, expectedDoc }) => { let event; beforeEach(() => { event = buildClipboardEvent({ eventName }); jest.spyOn(event, 'preventDefault'); jest.spyOn(event, 'stopPropagation'); tiptapEditor.commands.insertContent(PARAGRAPH_HTML); tiptapEditor.commands.selectAll(); tiptapEditor.view.dispatchEvent(event); }); it('prevents default', () => { expect(event.preventDefault).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled(); }); it('sets the clipboard data', () => { expect(event.clipboardData.setData).toHaveBeenCalledWith( 'text/plain', 'Some text with bold and italic text.', ); expect(event.clipboardData.setData).toHaveBeenCalledWith('text/html', PARAGRAPH_HTML); expect(event.clipboardData.setData).toHaveBeenCalledWith( 'text/x-gfm', 'Some text with **bold** and _italic_ text.', ); }); it('modifies the document', () => { expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc().toJSON()); }); }); describe('when copying content with a single table cell', () => { it('sets the clipboard data properly', () => { const event = buildClipboardEvent({ eventName: 'copy' }); tiptapEditor.commands.insertContent('Cell 1 |
Cell 1 | Cell 2 |
Cell 1 | Cell 2 |
bold text
'; }); it('inserts the block content after the existing paragraph', async () => { const expectedDoc = doc( p('Initial text and'), heading({ level: 1 }, 'Heading'), p(bold('bold text')), ); tiptapEditor.commands.setContent('Initial text and '); await triggerPasteEventHandler(buildClipboardEvent()); await resolveRenderMarkdownPromiseAndWait(resolvedValue); expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON()); }); }); }); describe('when pasting html content', () => { it('strips out any stray div, pre, span tags', async () => { const resolvedValue = 'some code
';
const expectedDoc = doc(p(bold('bold text')), p('some code'));
await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/html'],
data: {
'text/html':
'some code
',
},
}),
);
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
});
describe('when pasting text/x-gfm', () => {
it('processes the content as markdown, even if html content exists', async () => {
const resolvedValue = 'bold text';
const expectedDoc = doc(p(bold('bold text')));
await triggerPasteEventHandler(
buildClipboardEvent({
types: ['text/x-gfm', 'text/plain', 'text/html'],
data: {
'text/x-gfm': '**bold text**',
'text/plain': 'irrelevant text',
'text/html': '- Cat\n- Dog\n- Turtle\n
`,
},
}),
);
await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
});
describe('when pasting vscode-editor-data', () => {
it('pastes the content as a code block', async () => {
const resolvedValue =
'puts "Hello World"
bold text
'; const expectedDoc = doc(p(bold('bold text'))); await triggerPasteEventHandler( buildClipboardEvent({ types: ['vscode-editor-data', 'text/plain', 'text/html'], data: { 'vscode-editor-data': '{ "version": 1, "mode": "markdown" }', 'text/plain': '**bold text**', 'text/html': 'bold text
', }, }), ); await resolveRenderMarkdownPromiseAndWait(resolvedValue); expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON()); }); }); describe('when rendering markdown fails', () => { beforeEach(() => { renderMarkdown.mockRejectedValueOnce(); }); it(`triggers ${ALERT_EVENT} event`, async () => { await triggerPasteEventHandler(buildClipboardEvent()); await waitForPromises(); expect(eventHub.$emit).toHaveBeenCalledWith(ALERT_EVENT, { message: expect.any(String), variant: VARIANT_DANGER, }); }); }); }); });