Files
team/webapp/channels/jest.config.js
30381b8b42 MM-67755: WYSIWYG editor for message composition (#36143)
* MM-67755: WYSIWYG editor for message composition

- Add TipTap-based WYSIWYG editor as an opt-in setting
- Implement formatting bar with text styles dropdown, link popover
- Add custom list extension for Mattermost markdown compatibility
- Add suggestion list integration for mentions and emoji
- Add feature flag and user preference toggle
- Update e2e tests for WYSIWYG editor compatibility

Made-with: Cursor

* Fix package versions to match current master (11.7.0)

Made-with: Cursor

* Add missing PermissionPolicies feature flag from master

Made-with: Cursor

* Fix remaining WYSIWYG UX issues: toolbar overlap, autocomplete, emoji, tables

Made-with: Cursor

* Address CodeRabbit review: null guards, XSS fix, e2e tests, constants alignment

Made-with: Cursor

* UX fixes required by Matt

* linter

* fix e2e

* Fix CI failures: resolve merge conflicts with master

- Fix TS errors from useRewrite hook refactor (additionalControl → rewriteMenuProps)
- Fix onComposition → onCompositionUpdate prop rename on Textbox
- Remove accidentally re-added enzyme-to-json/serializer from jest config
- Extract new i18n strings for WYSIWYG editor components

Made-with: Cursor

* Fix lint: remove unused aiRewriteEnabled and rewriteMenuProps vars

Made-with: Cursor

* Fix jest and E2E test failures

- Add lowlight and ESM deps to jest transformIgnorePatterns
- Replace have.value with have.text for contenteditable elements
- Remove selectionStart/selectionEnd assertions for WYSIWYG editor

Made-with: Cursor

* e2e test

* Expose focus()/blur() on WysiwygEditor handle so focus hooks work

useTextboxFocus drives focus into the composer for many flows (channel
switch, RHS open, post-submit refocus, Reply shortcut, "type to focus
main input"). It calls textboxRef.current?.focus(), but textboxRef is
the legacy <Textbox> ref and is null when the WYSIWYG editor is the
mounted composer, so every focus call becomes a silent no-op.

Add focus() and blur() to WysiwygEditorHandle, backed by editor.commands
.focus()/blur(), and pass wysiwygRef into useTextboxFocus so it picks
whichever ref is mounted. The hook keeps its existing signature for
legacy callers; wysiwygRef is optional.

Verified locally on the WYSIWYG branch:
- messaging/focus_move_spec (3/6 failing -> 6/6 passing)
- messaging/message_edit_post_clear_text_spec (0/1 -> 1/1)
- messaging/message_edit_post_history_spec (0/3 -> 1/3)
- keyboard_shortcuts/dot_menu_spec gets past the Reply-focus assertion

* e2e: coerce postMessage args to string in WYSIWYG branch

The legacy textarea path used cy.invoke('val', message) which silently
coerced numbers/undefined to strings. The WYSIWYG branch added in the
previous refactor calls message.replace() and crashes with
"TypeError: message.replace is not a function" whenever a spec passes a
non-string value, e.g. cy.uiPostMessageQuickly(0) or before-each hooks
that pass undefined.

Funnel everything through asPostMessage() so the WYSIWYG branch is just
as forgiving as the textarea branch. Loosen postMessage /
postMessageReplyInRHS / uiPostMessageQuickly typings to `unknown` to
match what callers actually pass.

Fixes failures in: copy_post_text_spec, save_post_spec,
search_filter/{after,before,future_date}_spec, and
scroll/default_images_collapsed_spec.

* filtetr by feature flag for now

* coderabbit ai feedback

* coderabbitai comments

* align with code conventions

* fix e2e test

* PR comments

* fix i18n

* anchor link popover to editor selectio

* UX issues

* regression

* Final UX feedback

* linter

* e2e test rename

* Final UX

* Fix playwright lint

* Align e2e mocks with master

* Disable feature flag

* Sanitize urls before handling actions with link popove

---------

Co-authored-by: Nevyana Angelova <[email protected]>

Co-authored-by: Mattermost Build <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 15:11:30 +00:00

60 lines
2.3 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/** @type {import('jest').Config} */
const config = {
testPathIgnorePatterns: ['/node_modules/'],
clearMocks: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
],
coveragePathIgnorePatterns: [
'/node_modules/',
'mattermost-redux/src/selectors/create_selector',
],
coverageReporters: ['json', 'lcov', 'text-summary'],
fakeTimers: {
doNotFake: ['performance'],
},
moduleNameMapper: {
'^@mattermost/(components)$': '<rootDir>/../platform/$1/src',
'^@mattermost/(client)$': '<rootDir>/../platform/$1/src',
'^@mattermost/(types)/(.*)$': '<rootDir>/../platform/$1/src/$2',
'^@mattermost/shared/(.*)$': '<rootDir>/../platform/shared/src/$1',
'^mattermost-redux/test/(.*)$':
'<rootDir>/src/packages/mattermost-redux/test/$1',
'^mattermost-redux/(.*)$': '<rootDir>/src/packages/mattermost-redux/src/$1',
'^pdfjs-dist/.*': '<rootDir>/src/tests/pdfjs_mock.ts',
'^.+\\.(jpg|jpeg|png|apng|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/src/tests/image_url_mock.json',
'^.+\\.(css|less|scss)$': 'identity-obj-proxy',
'^.*i18n.*\\.(json)$': '<rootDir>/src/tests/i18n_mock.json',
},
moduleDirectories: ['src', 'node_modules'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
transformIgnorePatterns: [
'node_modules/(?!react-native|react-router|pdfjs-dist|p-queue|p-timeout|@mattermost/compass-icons|cidr-regex|ip-regex|serialize-error|@tiptap|marked|lowlight|devlop|fault|hast-util-to-text|@types/hast|unist-util-find-after)',
],
transform: {
'^.+\\.(js|jsx|ts|tsx|mjs)$': 'babel-jest',
},
setupFiles: ['jest-canvas-mock'],
setupFilesAfterEnv: ['<rootDir>/src/tests/setup_jest.ts'],
testEnvironment: 'jsdom',
testTimeout: 60000,
testEnvironmentOptions: {
url: 'http://localhost:8065',
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
snapshotFormat: {
escapeString: true,
printBasicPrototype: true,
},
};
module.exports = config;