diff --git a/jest.config.js b/jest.config.js index aa6e5cd681..f30a6cadc5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,7 +21,9 @@ */ const ignorePatterns = [ + 'ansi-regex', 'bail', + 'char-regex', 'comma-separated-tokens', 'decode-named-character-reference', 'escape-string-regexp', @@ -33,6 +35,9 @@ const ignorePatterns = [ 'rehype-*', 'remark-*', 'space-separated-tokens', + 'string-length', + 'strip-ansi', + 'tributejs', 'trim-lines', 'trough', 'unified', @@ -62,6 +67,10 @@ module.exports = { '/node_modules/(?!(' + ignorePatterns.join('|') + '))', ], + moduleNameMapper: { + '\\.(css|scss)$': 'jest-transform-stub', + }, + snapshotSerializers: [ '/node_modules/jest-serializer-vue', ], diff --git a/src/components/NcRichContenteditable/NcRichContenteditable.vue b/src/components/NcRichContenteditable/NcRichContenteditable.vue index 0d761a8439..98a61ac3ad 100644 --- a/src/components/NcRichContenteditable/NcRichContenteditable.vue +++ b/src/components/NcRichContenteditable/NcRichContenteditable.vue @@ -162,6 +162,8 @@ export default { role="textbox" @input="onInput" v-on="$listeners" + @compositionstart="isComposing = true" + @compositionend="isComposing = false" @keydown.delete="onDelete" @keydown.enter.exact="onEnter" @keydown.ctrl.enter.exact.stop.prevent="onCtrlEnter" @@ -364,6 +366,9 @@ export default { // serves no other purpose than to check whether the // content is empty or not localValue: this.value, + + // Is in text composition session in IME + isComposing: false, } }, @@ -655,17 +660,20 @@ export default { event.preventDefault() } }, - /** * Enter key pressed. Submits if not multiline * * @param {Event} event the keydown event */ onEnter(event) { - // Prevent submitting if autocompletion menu - // is opened or length is over maxlength - if (this.multiline || this.isOverMaxlength - || this.autocompleteTribute.isActive || this.emojiTribute.isActive || this.linkTribute.isActive) { + // Prevent submitting if multiline + // or length is over maxlength + // or autocompletion menu is opened + // or in a text composition session with IME + if (this.multiline + || this.isOverMaxlength + || this.autocompleteTribute.isActive || this.emojiTribute.isActive || this.linkTribute.isActive + || this.isComposing) { return } diff --git a/tests/unit/components/NcRichContenteditable/NcRichContenteditable.spec.js b/tests/unit/components/NcRichContenteditable/NcRichContenteditable.spec.js new file mode 100644 index 0000000000..69c679da83 --- /dev/null +++ b/tests/unit/components/NcRichContenteditable/NcRichContenteditable.spec.js @@ -0,0 +1,122 @@ +import { mount } from '@vue/test-utils' +import NcRichContenteditable from '../../../../src/components/NcRichContenteditable/NcRichContenteditable.vue' +import Tribute from 'tributejs/dist/tribute.esm.js' + +// FIXME: find a way to use Tribute in JSDOM or test with e2e +jest.mock('tributejs/dist/tribute.esm.js') +Tribute.mockImplementation(() => ({ + attach: jest.fn(), + detach: jest.fn(), +})) + +/** + * Mount NcRichContentEditable + * + * @param {object} options mount options + * @param {object} options.propsData mount options.propsData + * @param {object} options.listeners mount options.listeners + * @param {object} options.attrs mount options.attrs + * @return {object} + */ +function mountNcRichContenteditable({ propsData, listeners, attrs } = {}) { + let currentValue = propsData?.value + + const wrapper = mount(NcRichContenteditable, { + propsData: { + value: currentValue, + ...propsData, + }, + listeners: { + 'update:value': ($event) => { + currentValue = $event + wrapper.setProps({ value: $event }) + }, + ...listeners, + }, + attrs: { + ...attrs, + }, + attachTo: document.body, + }) + + const getCurrentValue = () => currentValue + + const inputValue = async (newValue) => { + wrapper.element.innerHTML += newValue + await wrapper.trigger('input') + } + + return { + wrapper, + getCurrentValue, + inputValue, + } +} + +describe('NcRichContenteditable', () => { + it('should update value during input', async () => { + const { wrapper, inputValue } = mountNcRichContenteditable() + const TEST_TEXT = 'Test Text' + await inputValue('Test Text') + expect(wrapper.emitted('update:value')).toBeDefined() + expect(wrapper.emitted('update:value').at(-1)[0]).toBe(TEST_TEXT) + }) + + it('should not emit "submit" during input', async () => { + const { wrapper, inputValue } = mountNcRichContenteditable() + await inputValue('Test Text') + expect(wrapper.emitted('submit')).not.toBeDefined() + }) + + it('should emit "paste" on past', async () => { + const { wrapper } = mountNcRichContenteditable() + await wrapper.trigger('paste', { clipboardData: { getData: () => 'PASTED_TEXT', files: [], items: {} } }) + expect(wrapper.emitted('paste')).toBeDefined() + expect(wrapper.emitted('paste')).toHaveLength(1) + }) + + it('should emit "submit" on Enter', async () => { + const { wrapper, inputValue } = mountNcRichContenteditable() + + await inputValue('Test Text') + + await wrapper.trigger('keydown', { keyCode: 13 }) // Enter + + expect(wrapper.emitted('submit')).toBeDefined() + expect(wrapper.emitted('submit')).toHaveLength(1) + }) + + it('should not emit "submit" on Enter during composition session', async () => { + const { wrapper, inputValue } = mountNcRichContenteditable() + + await wrapper.trigger('compositionstart') + await inputValue('猫') + await wrapper.trigger('keydown', { keyCode: 13 }) // Enter + await wrapper.trigger('compositionend') + await inputValue(' - means "Cat"') + await wrapper.trigger('keydown', { keyCode: 13 }) // Enter + + expect(wrapper.emitted('submit')).toBeDefined() + expect(wrapper.emitted('submit')).toHaveLength(1) + }) + + it('should proxy component events listeners to native event handlers', async () => { + const handlers = { + focus: jest.fn(), + paste: jest.fn(), + blur: jest.fn(), + } + const { wrapper } = mountNcRichContenteditable({ + listeners: handlers, + }) + + await wrapper.trigger('focus') + await wrapper.trigger('paste', { clipboardData: { getData: () => 'PASTED_TEXT', files: [], items: {} } }) + await wrapper.trigger('blur') + + expect(handlers.focus).toHaveBeenCalledTimes(1) + // FIXME: past event handler is always called twice + // expect(handlers.paste).toHaveBeenCalledTimes(1) + expect(handlers.blur).toHaveBeenCalledTimes(1) + }) +})