diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..c928ba1b25 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,91 @@ +/** + * @copyright Copyright (c) 2020 Marco Ambrosini + * + * @author Marco Ambrosini + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +const ignorePatterns = [ + 'ansi-regex', + 'bail', + 'char-regex', + 'comma-separated-tokens', + 'decode-named-character-reference', + 'escape-string-regexp', + 'hast-*', + 'is-*', + 'mdast-util-*', + 'micromark', + 'property-information', + 'rehype-*', + 'remark-*', + 'space-separated-tokens', + 'string-length', + 'strip-ansi', + 'tributejs', + 'trim-lines', + 'trough', + 'unified', + 'unist*', + 'vfile', + 'vue-material-design-icons', + 'web-namespaces', +] + +module.exports = { + moduleFileExtensions: [ + 'js', + 'vue', + ], + + testEnvironment: 'jsdom', + setupFilesAfterEnv: [ + './tests/setup.js', + ], + + transform: { + '^.+\\.js$': 'babel-jest', + '^.+\\.vue$': '@vue/vue2-jest', + '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', + }, + transformIgnorePatterns: [ + '/node_modules/(?!(' + ignorePatterns.join('|') + '))', + ], + + moduleNameMapper: { + '\\.(css|scss)$': 'jest-transform-stub', + }, + + snapshotSerializers: [ + '/node_modules/jest-serializer-vue', + ], + + coverageDirectory: './coverage/', + collectCoverage: false, + collectCoverageFrom: [ + '/src/**/*.{js,vue}', + '!**/node_modules/**', + ], + coverageReporters: [ + 'json', + 'text', + 'html', + 'lcov', + 'clover', + ], +} diff --git a/package-lock.json b/package-lock.json index fe38dc3bc4..991688d3b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "jest-transform-stub": "^2.0.0", "resolve-url-loader": "^5.0.0", "sanitize-filename": "^1.6.3", + "ts-node": "^10.9.1", "url-loader": "^4.1.1", "vue-eslint-parser": "^9.0.3", "vue-styleguidist": "~4.71.1", @@ -1942,6 +1943,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@cypress/request": { "version": "2.88.10", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz", @@ -3486,6 +3509,30 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -5236,6 +5283,12 @@ } ] }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -7966,6 +8019,12 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -16469,6 +16528,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -23480,6 +23545,58 @@ "integrity": "sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==", "dev": true }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", @@ -24197,6 +24314,12 @@ "dev": true, "peer": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -27371,6 +27494,15 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -28699,6 +28831,27 @@ "dev": true, "optional": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@cypress/request": { "version": "2.88.10", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz", @@ -29861,6 +30014,30 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -31265,6 +31442,12 @@ "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -33393,6 +33576,12 @@ "sha.js": "^2.4.8" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -39857,6 +40046,12 @@ "semver": "^6.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -45190,6 +45385,35 @@ "integrity": "sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==", "dev": true }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, "tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", @@ -45721,6 +45945,12 @@ "dev": true, "peer": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -48138,6 +48368,12 @@ "fd-slicer": "~1.1.0" } }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b8d71856db..1834fd69ea 100644 --- a/package.json +++ b/package.json @@ -114,46 +114,13 @@ "jest-transform-stub": "^2.0.0", "resolve-url-loader": "^5.0.0", "sanitize-filename": "^1.6.3", + "ts-node": "^10.9.1", "url-loader": "^4.1.1", "vue-eslint-parser": "^9.0.3", "vue-styleguidist": "~4.71.1", "vue-template-compiler": "^2.7.14", "webpack-node-externals": "^3.0.0" }, - "jest": { - "moduleFileExtensions": [ - "js", - "vue" - ], - "testEnvironment": "jsdom", - "transform": { - "^.+\\.js$": "babel-jest", - "^.+\\.vue$": "@vue/vue2-jest", - ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub" - }, - "transformIgnorePatterns": [ - "/node_modules/(?!(vue-material-design-icons)|(unist*)|(unified)|(bail)|(remark*)|(is-*)|(trough)|(vfile)|(mdast*)|(micromark)|(decode-named-character-reference)|(trim-lines)|(rehype*)|(hast-*)|(property-information)|(space-separated-tokens)|(comma-separated-tokens)|(web-namespaces))" - ], - "snapshotSerializers": [ - "/node_modules/jest-serializer-vue" - ], - "setupFilesAfterEnv": [ - "./tests/setup.js" - ], - "coverageDirectory": "./coverage/", - "collectCoverage": false, - "collectCoverageFrom": [ - "/src/**/*.{js,vue}", - "!**/node_modules/**" - ], - "coverageReporters": [ - "json", - "text", - "html", - "lcov", - "clover" - ] - }, "browserslist": [ "extends @nextcloud/browserslist-config" ] diff --git a/src/components/NcRichContenteditable/NcRichContenteditable.vue b/src/components/NcRichContenteditable/NcRichContenteditable.vue index 3f8a107554..ee3fe938ea 100644 --- a/src/components/NcRichContenteditable/NcRichContenteditable.vue +++ b/src/components/NcRichContenteditable/NcRichContenteditable.vue @@ -162,6 +162,8 @@ export default { role="textbox" v-on="listeners" @input="onInput" + @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, } }, @@ -672,17 +677,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..78ec12de09 --- /dev/null +++ b/tests/unit/components/NcRichContenteditable/NcRichContenteditable.spec.js @@ -0,0 +1,121 @@ +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) + expect(handlers.paste).toHaveBeenCalledTimes(1) + expect(handlers.blur).toHaveBeenCalledTimes(1) + }) +})