diff options
Diffstat (limited to 'spec/frontend/content_editor')
19 files changed, 399 insertions, 185 deletions
diff --git a/spec/frontend/content_editor/components/content_editor_error_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js index 8723fb5a338..2ddcd8f024e 100644 --- a/spec/frontend/content_editor/components/content_editor_error_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -1,11 +1,11 @@ import { GlAlert } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ContentEditorError from '~/content_editor/components/content_editor_error.vue'; +import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; import { createTestEditor, emitEditorEvent } from '../test_utils'; -describe('content_editor/components/content_editor_error', () => { +describe('content_editor/components/content_editor_alert', () => { let wrapper; let tiptapEditor; @@ -14,7 +14,7 @@ describe('content_editor/components/content_editor_error', () => { const createWrapper = async () => { tiptapEditor = createTestEditor(); - wrapper = shallowMountExtended(ContentEditorError, { + wrapper = shallowMountExtended(ContentEditorAlert, { provide: { tiptapEditor, }, @@ -28,22 +28,28 @@ describe('content_editor/components/content_editor_error', () => { wrapper.destroy(); }); - it('renders error when content editor emits an error event', async () => { - const error = 'error message'; + it.each` + variant | message + ${'danger'} | ${'An error occurred'} + ${'warning'} | ${'A warning'} + `( + 'renders error when content editor emits an error event for variant: $variant', + async ({ message, variant }) => { + createWrapper(); - createWrapper(); - - await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } }); + await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } }); - expect(findErrorAlert().text()).toBe(error); - }); + expect(findErrorAlert().text()).toBe(message); + expect(findErrorAlert().attributes().variant).toBe(variant); + }, + ); it('allows dismissing the error', async () => { - const error = 'error message'; + const message = 'error message'; createWrapper(); - await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } }); + await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } }); findErrorAlert().vm.$emit('dismiss'); diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 3d1ef03083d..9a772c41e52 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -3,7 +3,7 @@ import { EditorContent } from '@tiptap/vue-2'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContentEditor from '~/content_editor/components/content_editor.vue'; -import ContentEditorError from '~/content_editor/components/content_editor_error.vue'; +import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue'; import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue'; @@ -111,10 +111,10 @@ describe('ContentEditor', () => { ]); }); - it('renders content_editor_error component', () => { + it('renders content_editor_alert component', () => { createWrapper(); - expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true); + expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true); }); describe('when loading content', () => { diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js index e48f59f6d9c..6017a145a87 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js @@ -11,13 +11,13 @@ jest.mock('prosemirror-tables'); describe('content/components/wrappers/table_cell_base', () => { let wrapper; let editor; - let getPos; + let node; const createWrapper = async (propsData = { cellType: 'td' }) => { wrapper = shallowMountExtended(TableCellBaseWrapper, { propsData: { editor, - getPos, + node, ...propsData, }, }); @@ -36,7 +36,7 @@ describe('content/components/wrappers/table_cell_base', () => { const setCurrentPositionInCell = () => { const { $cursor } = editor.state.selection; - getPos.mockReturnValue($cursor.pos - $cursor.parentOffset - 1); + jest.spyOn($cursor, 'node').mockReturnValue(node); }; const mockDropdownHide = () => { /* @@ -48,7 +48,7 @@ describe('content/components/wrappers/table_cell_base', () => { }; beforeEach(() => { - getPos = jest.fn(); + node = {}; editor = createTestEditor({}); }); diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js index 5d26c44ba03..2aefbc77545 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js @@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils'; describe('content/components/wrappers/table_cell_body', () => { let wrapper; let editor; - let getPos; + let node; const createWrapper = async () => { wrapper = shallowMount(TableCellBodyWrapper, { propsData: { editor, - getPos, + node, }, }); }; beforeEach(() => { - getPos = jest.fn(); + node = {}; editor = createTestEditor({}); }); @@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_body', () => { createWrapper(); expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({ editor, - getPos, + node, cellType: 'td', }); }); diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js index e561191418d..e48df8734a6 100644 --- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js +++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js @@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils'; describe('content/components/wrappers/table_cell_header', () => { let wrapper; let editor; - let getPos; + let node; const createWrapper = async () => { wrapper = shallowMount(TableCellHeaderWrapper, { propsData: { editor, - getPos, + node, }, }); }; beforeEach(() => { - getPos = jest.fn(); + node = {}; editor = createTestEditor({}); }); @@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_header', () => { createWrapper(); expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({ editor, - getPos, + node, cellType: 'th', }); }); diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index d4f05a25bd6..d2d2cd98a78 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -74,10 +74,10 @@ describe('content_editor/extensions/attachment', () => { }); it.each` - eventType | propName | eventData | output - ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [attachmentFile] } }} | ${true} - ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [] } }} | ${undefined} - ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { files: [attachmentFile] } }} | ${true} + eventType | propName | eventData | output + ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [attachmentFile] } }} | ${true} + ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined} + ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true} `('handles $eventType properly', ({ eventType, propName, eventData, output }) => { const event = Object.assign(new Event(eventType), eventData); const handled = tiptapEditor.view.someProp(propName, (eventHandler) => { @@ -157,11 +157,11 @@ describe('content_editor/extensions/attachment', () => { }); }); - it('emits an error event that includes an error message', (done) => { + it('emits an alert event that includes an error message', (done) => { tiptapEditor.commands.uploadAttachment({ file: imageFile }); - tiptapEditor.on('error', ({ error }) => { - expect(error).toBe('An error occurred while uploading the image. Please try again.'); + tiptapEditor.on('alert', ({ message }) => { + expect(message).toBe('An error occurred while uploading the image. Please try again.'); done(); }); }); @@ -233,11 +233,11 @@ describe('content_editor/extensions/attachment', () => { }); }); - it('emits an error event that includes an error message', (done) => { + it('emits an alert event that includes an error message', (done) => { tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); - tiptapEditor.on('error', ({ error }) => { - expect(error).toBe('An error occurred while uploading the file. Please try again.'); + tiptapEditor.on('alert', ({ message }) => { + expect(message).toBe('An error occurred while uploading the file. Please try again.'); done(); }); }); diff --git a/spec/frontend/content_editor/extensions/blockquote_spec.js b/spec/frontend/content_editor/extensions/blockquote_spec.js index c5b5044352d..1644647ba69 100644 --- a/spec/frontend/content_editor/extensions/blockquote_spec.js +++ b/spec/frontend/content_editor/extensions/blockquote_spec.js @@ -1,19 +1,37 @@ -import { multilineInputRegex } from '~/content_editor/extensions/blockquote'; +import Blockquote from '~/content_editor/extensions/blockquote'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; describe('content_editor/extensions/blockquote', () => { - describe.each` - input | matches - ${'>>> '} | ${true} - ${' >>> '} | ${true} - ${'\t>>> '} | ${true} - ${'>> '} | ${false} - ${'>>>x '} | ${false} - ${'> '} | ${false} - `('multilineInputRegex', ({ input, matches }) => { - it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => { - const match = new RegExp(multilineInputRegex).test(input); + let tiptapEditor; + let doc; + let p; + let blockquote; - expect(match).toBe(matches); - }); + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Blockquote] }); + + ({ + builders: { doc, p, blockquote }, + } = createDocBuilder({ + tiptapEditor, + names: { + blockquote: { nodeType: Blockquote.name }, + }, + })); + }); + + it.each` + input | insertedNode + ${'>>> '} | ${() => blockquote({ multiline: true }, p())} + ${'> '} | ${() => blockquote(p())} + ${' >>> '} | ${() => blockquote({ multiline: true }, p())} + ${'>> '} | ${() => p()} + ${'>>>x '} | ${() => p()} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { + const expectedDoc = doc(insertedNode()); + + triggerNodeInputRule({ tiptapEditor, inputRuleText: input }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); }); diff --git a/spec/frontend/content_editor/extensions/emoji_spec.js b/spec/frontend/content_editor/extensions/emoji_spec.js index c1b8dc9bdbb..939c46e991a 100644 --- a/spec/frontend/content_editor/extensions/emoji_spec.js +++ b/spec/frontend/content_editor/extensions/emoji_spec.js @@ -1,6 +1,6 @@ import { initEmojiMock } from 'helpers/emoji'; import Emoji from '~/content_editor/extensions/emoji'; -import { createTestEditor, createDocBuilder } from '../test_utils'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; describe('content_editor/extensions/emoji', () => { let tiptapEditor; @@ -28,18 +28,16 @@ describe('content_editor/extensions/emoji', () => { describe('when typing a valid emoji input rule', () => { it('inserts an emoji node', () => { - const { view } = tiptapEditor; - const { selection } = view.state; const expectedDoc = doc( p( ' ', emoji({ moji: '❤', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }), ), ); - // Triggers the event handler that input rules listen to - view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:')); - expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + triggerNodeInputRule({ tiptapEditor, inputRuleText: ':heart:' }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); }); diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js new file mode 100644 index 00000000000..517f6947b9a --- /dev/null +++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js @@ -0,0 +1,30 @@ +import Frontmatter from '~/content_editor/extensions/frontmatter'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; + +describe('content_editor/extensions/frontmatter', () => { + let tiptapEditor; + let doc; + let p; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Frontmatter] }); + + ({ + builders: { doc, p }, + } = createDocBuilder({ + tiptapEditor, + names: { + frontmatter: { nodeType: Frontmatter.name }, + }, + })); + }); + + it('does not insert a frontmatter block when executing code block input rule', () => { + const expectedDoc = doc(p('')); + const inputRuleText = '``` '; + + triggerNodeInputRule({ tiptapEditor, inputRuleText }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); +}); diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js index a1bc7f0e8ed..322c04a42e1 100644 --- a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js +++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js @@ -1,20 +1,39 @@ -import { hrInputRuleRegExp } from '~/content_editor/extensions/horizontal_rule'; +import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; describe('content_editor/extensions/horizontal_rule', () => { - describe.each` - input | matches - ${'---'} | ${true} - ${'--'} | ${false} - ${'---x'} | ${false} - ${' ---x'} | ${false} - ${' --- '} | ${false} - ${'x---x'} | ${false} - ${'x---'} | ${false} - `('hrInputRuleRegExp', ({ input, matches }) => { - it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => { - const match = new RegExp(hrInputRuleRegExp).test(input); + let tiptapEditor; + let doc; + let p; + let horizontalRule; - expect(match).toBe(matches); - }); + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [HorizontalRule] }); + + ({ + builders: { doc, p, horizontalRule }, + } = createDocBuilder({ + tiptapEditor, + names: { + horizontalRule: { nodeType: HorizontalRule.name }, + }, + })); + }); + + it.each` + input | insertedNodes + ${'---'} | ${() => [p(), horizontalRule()]} + ${'--'} | ${() => [p()]} + ${'---x'} | ${() => [p()]} + ${' ---x'} | ${() => [p()]} + ${' --- '} | ${() => [p()]} + ${'x---x'} | ${() => [p()]} + ${'x---'} | ${() => [p()]} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNodes }) => { + const expectedDoc = doc(...insertedNodes()); + + triggerNodeInputRule({ tiptapEditor, inputRuleText: input }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); }); diff --git a/spec/frontend/content_editor/extensions/inline_diff_spec.js b/spec/frontend/content_editor/extensions/inline_diff_spec.js index 63cdf665e7f..99c559a20b1 100644 --- a/spec/frontend/content_editor/extensions/inline_diff_spec.js +++ b/spec/frontend/content_editor/extensions/inline_diff_spec.js @@ -1,27 +1,43 @@ -import { inputRegexAddition, inputRegexDeletion } from '~/content_editor/extensions/inline_diff'; +import InlineDiff from '~/content_editor/extensions/inline_diff'; +import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils'; describe('content_editor/extensions/inline_diff', () => { - describe.each` - inputRegex | description | input | matches - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+world+}'} | ${true} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+ world +}'} | ${true} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello {+ world+}'} | ${true} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello world +}'} | ${true} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello with \nnewline+}'} | ${false} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+open only'} | ${false} - ${inputRegexAddition} | ${'inputRegexAddition'} | ${'close only+}'} | ${false} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{-world-}'} | ${true} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{- world -}'} | ${true} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello {- world-}'} | ${true} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-hello world -}'} | ${true} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{+hello with \nnewline+}'} | ${false} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-open only'} | ${false} - ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'close only-}'} | ${false} - `('$description', ({ inputRegex, input, matches }) => { - it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => { - const match = new RegExp(inputRegex).test(input); + let tiptapEditor; + let doc; + let p; + let inlineDiff; - expect(match).toBe(matches); - }); + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [InlineDiff] }); + ({ + builders: { doc, p, inlineDiff }, + } = createDocBuilder({ + tiptapEditor, + names: { + inlineDiff: { markType: InlineDiff.name }, + }, + })); + }); + + it.each` + input | insertedNode + ${'hello{+world+}'} | ${() => p('hello', inlineDiff('world'))} + ${'hello{+ world +}'} | ${() => p('hello', inlineDiff(' world '))} + ${'{+hello with \nnewline+}'} | ${() => p('{+hello with newline+}')} + ${'{+open only'} | ${() => p('{+open only')} + ${'close only+}'} | ${() => p('close only+}')} + ${'hello{-world-}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, 'world'))} + ${'hello{- world -}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, ' world '))} + ${'hello {- world-}'} | ${() => p('hello ', inlineDiff({ type: 'deletion' }, ' world'))} + ${'{-hello world -}'} | ${() => p(inlineDiff({ type: 'deletion' }, 'hello world '))} + ${'{-hello with \nnewline-}'} | ${() => p('{-hello with newline-}')} + ${'{-open only'} | ${() => p('{-open only')} + ${'close only-}'} | ${() => p('close only-}')} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { + const expectedDoc = doc(insertedNode()); + + triggerMarkInputRule({ tiptapEditor, inputRuleText: input }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); }); diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js index 026b2a06df3..ead898554d1 100644 --- a/spec/frontend/content_editor/extensions/link_spec.js +++ b/spec/frontend/content_editor/extensions/link_spec.js @@ -1,61 +1,46 @@ -import { - markdownLinkSyntaxInputRuleRegExp, - urlSyntaxRegExp, - extractHrefFromMarkdownLink, -} from '~/content_editor/extensions/link'; +import Link from '~/content_editor/extensions/link'; +import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils'; describe('content_editor/extensions/link', () => { - describe.each` - input | matches - ${'[gitlab](https://gitlab.com)'} | ${true} - ${'[documentation](readme.md)'} | ${true} - ${'[link 123](readme.md)'} | ${true} - ${'[link 123](read me.md)'} | ${true} - ${'text'} | ${false} - ${'documentation](readme.md'} | ${false} - ${'https://www.google.com'} | ${false} - `('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => { - it(`${matches ? 'matches' : 'does not match'} ${input}`, () => { - const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input); - - expect(Boolean(match?.groups.href)).toBe(matches); - }); + let tiptapEditor; + let doc; + let p; + let link; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Link] }); + ({ + builders: { doc, p, link }, + } = createDocBuilder({ + tiptapEditor, + names: { + link: { markType: Link.name }, + }, + })); }); - describe.each` - input | matches - ${'http://example.com '} | ${true} - ${'https://example.com '} | ${true} - ${'www.example.com '} | ${true} - ${'example.com/ab.html '} | ${false} - ${'text'} | ${false} - ${' http://example.com '} | ${true} - ${'https://www.google.com '} | ${true} - `('urlSyntaxRegExp', ({ input, matches }) => { - it(`${matches ? 'matches' : 'does not match'} ${input}`, () => { - const match = new RegExp(urlSyntaxRegExp).exec(input); - - expect(Boolean(match?.groups.href)).toBe(matches); - }); + afterEach(() => { + tiptapEditor.destroy(); }); - describe('extractHrefFromMarkdownLink', () => { - const input = '[gitlab](https://gitlab.com)'; - const href = 'https://gitlab.com'; - let match; - let result; - - beforeEach(() => { - match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input); - result = extractHrefFromMarkdownLink(match); - }); - - it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => { - expect(result).toEqual({ href }); - }); - - it('makes sure that url text is the last capture group', () => { - expect(match[match.length - 1]).toEqual('gitlab'); - }); + it.each` + input | insertedNode + ${'[gitlab](https://gitlab.com)'} | ${() => p(link({ href: 'https://gitlab.com' }, 'gitlab'))} + ${'[documentation](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'documentation'))} + ${'[link 123](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'link 123'))} + ${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))} + ${'text'} | ${() => p('text')} + ${'documentation](readme.md'} | ${() => p('documentation](readme.md')} + ${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))} + ${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))} + ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))} + ${'example.com/ab.html '} | ${() => p('example.com/ab.html')} + ${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { + const expectedDoc = doc(insertedNode()); + + triggerMarkInputRule({ tiptapEditor, inputRuleText: input }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); }); diff --git a/spec/frontend/content_editor/extensions/math_inline_spec.js b/spec/frontend/content_editor/extensions/math_inline_spec.js index 82eb85477de..abf10317b5a 100644 --- a/spec/frontend/content_editor/extensions/math_inline_spec.js +++ b/spec/frontend/content_editor/extensions/math_inline_spec.js @@ -1,5 +1,5 @@ import MathInline from '~/content_editor/extensions/math_inline'; -import { createTestEditor, createDocBuilder } from '../test_utils'; +import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils'; describe('content_editor/extensions/math_inline', () => { let tiptapEditor; @@ -26,16 +26,9 @@ describe('content_editor/extensions/math_inline', () => { ${'$`a^2`'} | ${() => p('$`a^2`')} ${'`a^2`$'} | ${() => p('`a^2`$')} `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { - const { view } = tiptapEditor; const expectedDoc = doc(insertedNode()); - tiptapEditor.chain().setContent(input).setTextSelection(0).run(); - - const { state } = tiptapEditor; - const { selection } = state; - - // Triggers the event handler that input rules listen to - view.someProp('handleTextInput', (f) => f(view, selection.from, input.length + 1, input)); + triggerMarkInputRule({ tiptapEditor, inputRuleText: input }); expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); diff --git a/spec/frontend/content_editor/extensions/table_of_contents_spec.js b/spec/frontend/content_editor/extensions/table_of_contents_spec.js index 83818899c17..0ddd88b39fe 100644 --- a/spec/frontend/content_editor/extensions/table_of_contents_spec.js +++ b/spec/frontend/content_editor/extensions/table_of_contents_spec.js @@ -1,13 +1,17 @@ import TableOfContents from '~/content_editor/extensions/table_of_contents'; -import { createTestEditor, createDocBuilder } from '../test_utils'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; -describe('content_editor/extensions/emoji', () => { +describe('content_editor/extensions/table_of_contents', () => { let tiptapEditor; - let builders; + let doc; + let tableOfContents; + let p; beforeEach(() => { tiptapEditor = createTestEditor({ extensions: [TableOfContents] }); - ({ builders } = createDocBuilder({ + ({ + builders: { doc, p, tableOfContents }, + } = createDocBuilder({ tiptapEditor, names: { tableOfContents: { nodeType: TableOfContents.name } }, })); @@ -15,20 +19,16 @@ describe('content_editor/extensions/emoji', () => { it.each` input | insertedNode - ${'[[_TOC_]]'} | ${'tableOfContents'} - ${'[TOC]'} | ${'tableOfContents'} - ${'[toc]'} | ${'p'} - ${'TOC'} | ${'p'} - ${'[_TOC_]'} | ${'p'} - ${'[[TOC]]'} | ${'p'} + ${'[[_TOC_]]'} | ${() => tableOfContents()} + ${'[TOC]'} | ${() => tableOfContents()} + ${'[toc]'} | ${() => p()} + ${'TOC'} | ${() => p()} + ${'[_TOC_]'} | ${() => p()} + ${'[[TOC]]'} | ${() => p()} `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { - const { doc } = builders; - const { view } = tiptapEditor; - const { selection } = view.state; - const expectedDoc = doc(builders[insertedNode]()); + const expectedDoc = doc(insertedNode()); - // Triggers the event handler that input rules listen to - view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input)); + triggerNodeInputRule({ tiptapEditor, inputRuleText: input }); expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); }); diff --git a/spec/frontend/content_editor/extensions/table_spec.js b/spec/frontend/content_editor/extensions/table_spec.js new file mode 100644 index 00000000000..121fe9192db --- /dev/null +++ b/spec/frontend/content_editor/extensions/table_spec.js @@ -0,0 +1,102 @@ +import Bold from '~/content_editor/extensions/bold'; +import BulletList from '~/content_editor/extensions/bullet_list'; +import ListItem from '~/content_editor/extensions/list_item'; +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 { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/table', () => { + let tiptapEditor; + let doc; + let p; + let table; + let tableHeader; + let tableCell; + let tableRow; + let initialDoc; + let mockAlert; + + beforeEach(() => { + tiptapEditor = createTestEditor({ + extensions: [Table, TableCell, TableRow, TableHeader, BulletList, Bold, ListItem], + }); + + ({ + builders: { doc, p, table, tableCell, tableHeader, tableRow }, + } = createDocBuilder({ + tiptapEditor, + names: { + bold: { markType: Bold.name }, + table: { nodeType: Table.name }, + tableHeader: { nodeType: TableHeader.name }, + tableCell: { nodeType: TableCell.name }, + tableRow: { nodeType: TableRow.name }, + bulletList: { nodeType: BulletList.name }, + listItem: { nodeType: ListItem.name }, + }, + })); + + initialDoc = doc( + table( + { isMarkdown: true }, + tableRow(tableHeader(p('This is')), tableHeader(p('a table'))), + tableRow(tableCell(p('this is')), tableCell(p('the first row'))), + ), + ); + + mockAlert = jest.fn(); + }); + + it('triggers a warning (just once) if the table is markdown, but the changes in the document will render an HTML table instead', () => { + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + tiptapEditor.on('alert', mockAlert); + + tiptapEditor.commands.setTextSelection({ from: 20, to: 22 }); + tiptapEditor.commands.toggleBulletList(); + + jest.advanceTimersByTime(1001); + expect(mockAlert).toHaveBeenCalled(); + + mockAlert.mockReset(); + + tiptapEditor.commands.setTextSelection({ from: 4, to: 6 }); + tiptapEditor.commands.toggleBulletList(); + + jest.advanceTimersByTime(1001); + expect(mockAlert).not.toHaveBeenCalled(); + }); + + it('does not trigger a warning if the table is markdown, and the changes in the document can generate a markdown table', () => { + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + tiptapEditor.on('alert', mockAlert); + + tiptapEditor.commands.setTextSelection({ from: 20, to: 22 }); + tiptapEditor.commands.toggleBold(); + + jest.advanceTimersByTime(1001); + expect(mockAlert).not.toHaveBeenCalled(); + }); + + it('does not trigger any warnings if the table is not markdown', () => { + initialDoc = doc( + table( + tableRow(tableHeader(p('This is')), tableHeader(p('a table'))), + tableRow(tableCell(p('this is')), tableCell(p('the first row'))), + ), + ); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + tiptapEditor.on('alert', mockAlert); + + tiptapEditor.commands.setTextSelection({ from: 20, to: 22 }); + tiptapEditor.commands.toggleBulletList(); + + jest.advanceTimersByTime(1001); + expect(mockAlert).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/content_editor/extensions/word_break_spec.js b/spec/frontend/content_editor/extensions/word_break_spec.js new file mode 100644 index 00000000000..23167269d7d --- /dev/null +++ b/spec/frontend/content_editor/extensions/word_break_spec.js @@ -0,0 +1,35 @@ +import WordBreak from '~/content_editor/extensions/word_break'; +import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils'; + +describe('content_editor/extensions/word_break', () => { + let tiptapEditor; + let doc; + let p; + let wordBreak; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [WordBreak] }); + + ({ + builders: { doc, p, wordBreak }, + } = createDocBuilder({ + tiptapEditor, + names: { + wordBreak: { nodeType: WordBreak.name }, + }, + })); + }); + + it.each` + input | insertedNode + ${'<wbr>'} | ${() => p(wordBreak())} + ${'<wbr'} | ${() => p()} + ${'wbr>'} | ${() => p()} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { + const expectedDoc = doc(insertedNode()); + + triggerNodeInputRule({ tiptapEditor, inputRuleText: input }); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); +}); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 33056ab9e4a..cfd93c2df10 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -34,10 +34,6 @@ import { createTestEditor, createDocBuilder } from '../test_utils'; jest.mock('~/emoji'); -jest.mock('~/content_editor/services/feature_flags', () => ({ - isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true), -})); - const tiptapEditor = createTestEditor({ extensions: [ Blockquote, diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js index afe09a75f16..459780cc7cf 100644 --- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js +++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js @@ -10,7 +10,7 @@ import Heading from '~/content_editor/extensions/heading'; import ListItem from '~/content_editor/extensions/list_item'; import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts'; import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys'; -import { createTestEditor } from '../test_utils'; +import { createTestEditor, triggerNodeInputRule } from '../test_utils'; describe('content_editor/services/track_input_rules_and_shortcuts', () => { let trackingSpy; @@ -70,14 +70,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => { describe('when creating a heading using an input rule', () => { it('sends a tracking event indicating that a heading was created using an input rule', async () => { const nodeName = Heading.name; - const { view } = editor; - const { selection } = view.state; - - // Triggers the event handler that input rules listen to - view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, '## ')); - - editor.chain().insertContent(HEADING_TEXT).run(); - + triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' }); expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, { label: CONTENT_EDITOR_TRACKING_LABEL, property: `${nodeName}`, diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js index cf5aa3f2938..b236c630e13 100644 --- a/spec/frontend/content_editor/test_utils.js +++ b/spec/frontend/content_editor/test_utils.js @@ -119,3 +119,26 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => { }, }; }; + +export const triggerNodeInputRule = ({ tiptapEditor, inputRuleText }) => { + const { view } = tiptapEditor; + const { state } = tiptapEditor; + const { selection } = state; + + // Triggers the event handler that input rules listen to + view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, inputRuleText)); +}; + +export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => { + const { view } = tiptapEditor; + + tiptapEditor.chain().setContent(inputRuleText).setTextSelection(0).run(); + + const { state } = tiptapEditor; + const { selection } = state; + + // Triggers the event handler that input rules listen to + view.someProp('handleTextInput', (f) => + f(view, selection.from, inputRuleText.length + 1, inputRuleText), + ); +}; |