* feat: add customizable signature position and separator options * fix: correct default value note for signatureSeparator and ensure reactivity * fix: correct watcher boolean conversion and add immediate ui_settings updates - Fix watchers to convert string props to boolean values for reactive refs - Add immediate event handlers for switch changes to update ui_settings in real-time - Ensure proper synchronization between switch states and user.ui_settings Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: split signature content and ui_settings updates to resolve persistence bug - Use updateUISettings store action for signature_position and signature_separator - Keep updateProfile for message_signature content only - Fixes FormData serialization issue that corrupted nested ui_settings object - Add diagnostic logging to verify data flow Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * clean: remove diagnostic console logging from updateSignature method - Remove temporary console.log statements added for verification - Keep core implementation that splits signature content and ui_settings updates - Keep console.error for proper error handling with eslint-disable comment - Implementation now ready for production use Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: updateUISettings call in updateSignature method * chore: move signature application to send-time and add button highlighting (#79) * fix: move signature application from editor manipulation to send-time - Remove addSignature/removeSignature/toggleSignatureInEditor from WootWriter - Remove signature logic from draft handling and canned response insertion - Apply signatures only in getMessagePayload during message sending - Add button highlighting for signature toggle when activated - Prevents signature duplication and persistence in editor content - Fixes signature position toggle bug Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: escape signature separator to prevent markdown setext heading interpretation - Escape '--' separator as '\--' in appendSignature to prevent H2 heading creation - Update removeSignature to handle escaped separators correctly - Fixes signature separator being rendered as markdown instead of plain text - Refactor nested ternary to fix ESLint error Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: prevent signature separator markdown interpretation in message processing - Add fix_signature_separator_markdown method to escape '--' separators - Update ensure_processed_message_content to fix separators before saving - Prevents signature separators from being interpreted as setext headings - Ensures correct message display in channels and email notifications Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: update separator format to use \n--\n instead of escaping - Change separator delimiter from '\--' to '\n--\n' format - Update removeSignature function to handle new separator format correctly - Simplify message processing since separators are already properly formatted - Ensures consistent separator handling across frontend and backend Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: update signature delimiter format to include extra new lines * chore: remove comment about signature application logic * refactor: remove unused method and comments related to signature separator markdown processing * chore: simplify slash command detection by using updatedMessage directly * refactor: remove signature logic from draft message handling * refactor: simplify body empty check by removing signature manipulation logic --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cayo@fazer.ai <cayoproliveira@gmail.com> * refactor: extract signature settings logic into a separate method * fix: handle nil ui_settings in signature position and separator methods * fix: update return value of findSignatureInBody to include position information * fix: update signature handling in findSignatureInBody and related methods * fix: adjust delimiter length handling in removeSignature function * test: add cases for appending, removing, and replacing signatures with various separators * test: add cases for signature position and separator handling * test: add cases for updating signature position and separator in ui_settings * fix: correct typo in comment for findSignatureInBody function * refactor: simplify translation function calls in MessageSignature component * chore: refactoring * chore: refactor * feat: switch -> select * chore: refactor and undo changes * chore: refactor and undo changes * chore: refactor * fix: remove old select component usage * chore: remove useless style --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
666 lines
19 KiB
JavaScript
666 lines
19 KiB
JavaScript
import {
|
|
findSignatureInBody,
|
|
appendSignature,
|
|
removeSignature,
|
|
replaceSignature,
|
|
cleanSignature,
|
|
extractTextFromMarkdown,
|
|
insertAtCursor,
|
|
findNodeToInsertImage,
|
|
setURLWithQueryAndSize,
|
|
getContentNode,
|
|
} from '../editorHelper';
|
|
import { EditorState } from '@chatwoot/prosemirror-schema';
|
|
import { EditorView } from '@chatwoot/prosemirror-schema';
|
|
import { Schema } from 'prosemirror-model';
|
|
|
|
// Define a basic ProseMirror schema
|
|
const schema = new Schema({
|
|
nodes: {
|
|
doc: { content: 'paragraph+' },
|
|
paragraph: {
|
|
content: 'inline*',
|
|
group: 'block',
|
|
toDOM: () => ['p', 0], // Represents a paragraph as a <p> tag in the DOM.
|
|
},
|
|
text: {
|
|
group: 'inline',
|
|
toDOM: node => node.text, // Represents text as its actual string value.
|
|
},
|
|
mention: {
|
|
attrs: {
|
|
userId: { default: '' },
|
|
userFullName: { default: '' },
|
|
mentionType: { default: 'user' },
|
|
},
|
|
inline: true,
|
|
group: 'inline',
|
|
toDOM: node => [
|
|
'span',
|
|
{ class: 'mention' },
|
|
`@${node.attrs.userFullName}`,
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Initialize a basic EditorState for testing
|
|
const createEditorState = (content = '') => {
|
|
if (!content) {
|
|
return EditorState.create({
|
|
schema,
|
|
doc: schema.node('doc', null, [schema.node('paragraph')]),
|
|
});
|
|
}
|
|
return EditorState.create({
|
|
schema,
|
|
doc: schema.node('doc', null, [
|
|
schema.node('paragraph', null, [schema.text(content)]),
|
|
]),
|
|
});
|
|
};
|
|
|
|
const NEW_SIGNATURE = 'This is a new signature';
|
|
|
|
const DOES_NOT_HAVE_SIGNATURE = {
|
|
'no signature': {
|
|
body: 'This is a test',
|
|
signature: 'This is a signature',
|
|
},
|
|
'text after signature': {
|
|
body: 'This is a test\n\n--\n\nThis is a signature\n\nThis is more text',
|
|
signature: 'This is a signature',
|
|
},
|
|
'signature has images': {
|
|
body: 'This is a test',
|
|
signature:
|
|
'Testing\n',
|
|
},
|
|
'signature has non commonmark syntax': {
|
|
body: 'This is a test',
|
|
signature: '- Signature',
|
|
},
|
|
'signature has trailing spaces': {
|
|
body: 'This is a test',
|
|
signature: '**hello** \n**world**',
|
|
},
|
|
};
|
|
|
|
const HAS_SIGNATURE = {
|
|
'signature at end': {
|
|
body: 'This is a test\n\n--\n\nThis is a signature',
|
|
signature: 'This is a signature',
|
|
},
|
|
'signature at end with spaces and new lines': {
|
|
body: 'This is a test\n\n--\n\nThis is a signature \n\n',
|
|
signature: 'This is a signature ',
|
|
},
|
|
'no text before signature': {
|
|
body: '\n\n--\n\nThis is a signature',
|
|
signature: 'This is a signature',
|
|
},
|
|
'signature has non-commonmark syntax': {
|
|
body: '\n\n--\n\n* Signature',
|
|
signature: '- Signature',
|
|
},
|
|
};
|
|
|
|
describe.skip('findSignatureInBody - SKIP(#78): Due to changes on append signature logic', () => {
|
|
it('returns -1 if there is no signature', () => {
|
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
|
expect(findSignatureInBody(body, signature).index).toBe(-1);
|
|
});
|
|
});
|
|
it('returns the index of the signature if there is one', () => {
|
|
Object.keys(HAS_SIGNATURE).forEach(key => {
|
|
const { body, signature } = HAS_SIGNATURE[key];
|
|
expect(findSignatureInBody(body, signature).index).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('appendSignature', () => {
|
|
it('appends the signature if it is not present', () => {
|
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
|
const cleanedSignature = cleanSignature(signature);
|
|
expect(
|
|
appendSignature(body, signature, {
|
|
position: 'bottom',
|
|
separator: '--',
|
|
}).includes(cleanedSignature)
|
|
).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('appends the signature at the top with -- separator', () => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
|
const cleanedSignature = cleanSignature(signature);
|
|
expect(
|
|
appendSignature(body, signature, {
|
|
position: 'top',
|
|
separator: '--',
|
|
})
|
|
).toBe(`${cleanedSignature}\n\n--\n\n${body}`);
|
|
});
|
|
|
|
it('appends the signature at the bottom with blank separator', () => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
|
const cleanedSignature = cleanSignature(signature);
|
|
expect(
|
|
appendSignature(body, signature, {
|
|
position: 'bottom',
|
|
separator: 'blank',
|
|
})
|
|
).toBe(`${body}\n\n${cleanedSignature}`);
|
|
});
|
|
|
|
it('appends the signature at the top with blank separator', () => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
|
const cleanedSignature = cleanSignature(signature);
|
|
expect(
|
|
appendSignature(body, signature, {
|
|
position: 'top',
|
|
separator: 'blank',
|
|
})
|
|
).toBe(`${cleanedSignature}\n\n${body}`);
|
|
});
|
|
|
|
it.skip('does not append signature if already present - SKIP(#78): Due to changes on append signature logic', () => {
|
|
Object.keys(HAS_SIGNATURE).forEach(key => {
|
|
const { body, signature } = HAS_SIGNATURE[key];
|
|
expect(appendSignature(body, signature)).toBe(body);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('cleanSignature', () => {
|
|
it('removes any instance of horizontal rule', () => {
|
|
const options = [
|
|
'---',
|
|
'***',
|
|
'___',
|
|
'- - -',
|
|
'* * *',
|
|
'_ _ _',
|
|
' ---',
|
|
'--- ',
|
|
' --- ',
|
|
'-----',
|
|
'*****',
|
|
'_____',
|
|
'- - - -',
|
|
'* * * * *',
|
|
'_ _ _ _ _ _',
|
|
' - - - - ',
|
|
' * * * * * ',
|
|
' _ _ _ _ _ _',
|
|
'- - - - -',
|
|
'* * * * * *',
|
|
'_ _ _ _ _ _ _',
|
|
];
|
|
options.forEach(option => {
|
|
expect(cleanSignature(option)).toBe('');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe.skip('removeSignature - SKIP(#78): Due to changes on append signature logic', () => {
|
|
it('does not remove signature if not present', () => {
|
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
|
expect(removeSignature(body, signature)).toBe(body);
|
|
});
|
|
});
|
|
it('removes signature if present at the end', () => {
|
|
const { body, signature } = HAS_SIGNATURE['signature at end'];
|
|
expect(removeSignature(body, signature, '--')).toBe('This is a test');
|
|
});
|
|
it('removes signature if present with spaces and new lines', () => {
|
|
const { body, signature } =
|
|
HAS_SIGNATURE['signature at end with spaces and new lines'];
|
|
expect(removeSignature(body, signature, '--')).toBe('This is a test');
|
|
});
|
|
it('removes signature if present without text before it', () => {
|
|
const { body, signature } = HAS_SIGNATURE['no text before signature'];
|
|
expect(removeSignature(body, signature)).toBe('\n\n');
|
|
});
|
|
it('removes just the delimiter if no signature is present', () => {
|
|
expect(removeSignature('This is a test\n\n--', 'This is a signature')).toBe(
|
|
'This is a test\n\n'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe.skip('replaceSignature - SKIP(#78): Due to changes on append signature logic', () => {
|
|
it('appends the new signature if not present', () => {
|
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
|
`${body}\n\n--\n\n${NEW_SIGNATURE}`
|
|
);
|
|
});
|
|
});
|
|
it('removes signature if present at the end', () => {
|
|
const { body, signature } = HAS_SIGNATURE['signature at end'];
|
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
|
`This is a test\n\n--\n\n${NEW_SIGNATURE}`
|
|
);
|
|
});
|
|
it('removes signature if present with spaces and new lines', () => {
|
|
const { body, signature } =
|
|
HAS_SIGNATURE['signature at end with spaces and new lines'];
|
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
|
`This is a test\n\n--\n\n${NEW_SIGNATURE}`
|
|
);
|
|
});
|
|
it('removes signature if present without text before it', () => {
|
|
const { body, signature } = HAS_SIGNATURE['no text before signature'];
|
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
|
`\n\n--\n\n${NEW_SIGNATURE}`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('extractTextFromMarkdown', () => {
|
|
it('should extract text from markdown and remove all images, code blocks, links, headers, bold, italic, lists etc.', () => {
|
|
const markdown = `
|
|
# Hello World
|
|
|
|
This is a **bold** text with a [link](https://example.com).
|
|
|
|
\`\`\`javascript
|
|
const foo = 'bar';
|
|
console.log(foo);
|
|
\`\`\`
|
|
|
|
Here's an image: 
|
|
|
|
- List item 1
|
|
- List item 2
|
|
|
|
*Italic text*
|
|
`;
|
|
|
|
const expected =
|
|
"Hello World\nThis is a bold text with a link.\nHere's an image:\nList item 1\nList item 2\nItalic text";
|
|
expect(extractTextFromMarkdown(markdown)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('insertAtCursor', () => {
|
|
it('should return undefined if editorView is not provided', () => {
|
|
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should unwrap doc nodes that are wrapped in a paragraph', () => {
|
|
const docNode = schema.node('doc', null, [
|
|
schema.node('paragraph', null, [schema.text('Hello')]),
|
|
]);
|
|
|
|
const editorState = createEditorState();
|
|
const editorView = new EditorView(document.body, { state: editorState });
|
|
|
|
insertAtCursor(editorView, docNode, 0);
|
|
|
|
// Check if node was unwrapped and inserted correctly
|
|
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello');
|
|
});
|
|
|
|
it('should insert node without replacing any content if "to" is not provided', () => {
|
|
const editorState = createEditorState();
|
|
const editorView = new EditorView(document.body, { state: editorState });
|
|
|
|
insertAtCursor(editorView, schema.text('Hello'), 0);
|
|
|
|
// Check if node was inserted correctly
|
|
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello');
|
|
});
|
|
|
|
it('should replace content between "from" and "to" with the provided node', () => {
|
|
const editorState = createEditorState('ReplaceMe');
|
|
const editorView = new EditorView(document.body, { state: editorState });
|
|
|
|
insertAtCursor(editorView, schema.text('Hello'), 0, 8);
|
|
|
|
// Check if content was replaced correctly
|
|
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello Me');
|
|
});
|
|
});
|
|
|
|
describe('findNodeToInsertImage', () => {
|
|
let mockEditorState;
|
|
|
|
beforeEach(() => {
|
|
mockEditorState = {
|
|
selection: {
|
|
$from: {
|
|
node: vi.fn(() => ({})),
|
|
},
|
|
from: 0,
|
|
},
|
|
schema: {
|
|
nodes: {
|
|
image: {
|
|
create: vi.fn(attrs => ({ type: { name: 'image' }, attrs })),
|
|
},
|
|
paragraph: {
|
|
create: vi.fn((_, node) => ({
|
|
type: { name: 'paragraph' },
|
|
content: [node],
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
it('should insert image directly into an empty paragraph', () => {
|
|
const mockNode = {
|
|
type: { name: 'paragraph' },
|
|
content: { size: 0, content: [] },
|
|
};
|
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
|
|
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
|
expect(result).toEqual({
|
|
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
|
|
pos: 0,
|
|
});
|
|
});
|
|
|
|
it('should insert image directly into a paragraph without an image but with other content', () => {
|
|
const mockNode = {
|
|
type: { name: 'paragraph' },
|
|
content: {
|
|
size: 1,
|
|
content: [
|
|
{
|
|
type: { name: 'text' },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
|
mockEditorState.selection.from = 1;
|
|
|
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
|
expect(result).toEqual({
|
|
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
|
|
pos: 2, // Because it should insert after the text, on a new line.
|
|
});
|
|
});
|
|
|
|
it("should wrap image in a new paragraph when the current node isn't a paragraph", () => {
|
|
const mockNode = {
|
|
type: { name: 'not-a-paragraph' },
|
|
content: { size: 0, content: [] },
|
|
};
|
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
|
|
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
|
expect(result.node.type.name).toBe('paragraph');
|
|
expect(result.node.content[0].type.name).toBe('image');
|
|
expect(result.node.content[0].attrs.src).toBe('image-url');
|
|
expect(result.pos).toBe(0);
|
|
});
|
|
|
|
it('should insert a new image directly into the paragraph that already contains an image', () => {
|
|
const mockNode = {
|
|
type: { name: 'paragraph' },
|
|
content: {
|
|
size: 1,
|
|
content: [
|
|
{
|
|
type: { name: 'image', attrs: { src: 'existing-image-url' } },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
|
mockEditorState.selection.from = 1;
|
|
|
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
|
expect(result.node.type.name).toBe('image');
|
|
expect(result.node.attrs.src).toBe('image-url');
|
|
expect(result.pos).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('setURLWithQueryAndSize', () => {
|
|
let selectedNode;
|
|
let editorView;
|
|
|
|
beforeEach(() => {
|
|
selectedNode = {
|
|
setAttribute: vi.fn(),
|
|
};
|
|
|
|
const tr = {
|
|
setNodeMarkup: vi.fn().mockReturnValue({
|
|
docChanged: true,
|
|
}),
|
|
};
|
|
|
|
const state = {
|
|
selection: { from: 0 },
|
|
tr,
|
|
};
|
|
|
|
editorView = {
|
|
state,
|
|
dispatch: vi.fn(),
|
|
};
|
|
});
|
|
|
|
it('updates the URL with the given size and updates the editor view', () => {
|
|
const size = { height: '20px' };
|
|
|
|
setURLWithQueryAndSize(selectedNode, size, editorView);
|
|
|
|
// Check if the editor view is updated
|
|
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('updates the URL with the given size and updates the editor view with original size', () => {
|
|
const size = { height: 'auto' };
|
|
|
|
setURLWithQueryAndSize(selectedNode, size, editorView);
|
|
|
|
// Check if the editor view is updated
|
|
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not update the editor view if the document has not changed', () => {
|
|
editorView.state.tr.setNodeMarkup = vi.fn().mockReturnValue({
|
|
docChanged: false,
|
|
});
|
|
|
|
const size = { height: '20px' };
|
|
|
|
setURLWithQueryAndSize(selectedNode, size, editorView);
|
|
|
|
// Check if the editor view dispatch was not called
|
|
expect(editorView.dispatch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not perform any operations if selectedNode is not provided', () => {
|
|
setURLWithQueryAndSize(null, { height: '20px' }, editorView);
|
|
|
|
// Ensure the dispatch method wasn't called
|
|
expect(editorView.dispatch).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getContentNode', () => {
|
|
let mockEditorView;
|
|
|
|
beforeEach(() => {
|
|
mockEditorView = {
|
|
state: {
|
|
schema: {
|
|
nodes: {
|
|
mention: {
|
|
create: vi.fn(attrs => ({
|
|
type: { name: 'mention' },
|
|
attrs,
|
|
})),
|
|
},
|
|
},
|
|
text: vi.fn(content => ({ type: { name: 'text' }, text: content })),
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
describe('mention node creation', () => {
|
|
it('creates a user mention node with correct attributes', () => {
|
|
const userContent = {
|
|
id: '123',
|
|
name: 'John Doe',
|
|
type: 'user',
|
|
};
|
|
|
|
const result = getContentNode(mockEditorView, 'mention', userContent, {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(
|
|
mockEditorView.state.schema.nodes.mention.create
|
|
).toHaveBeenCalledWith({
|
|
userId: '123',
|
|
userFullName: 'John Doe',
|
|
mentionType: 'user',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
node: {
|
|
type: { name: 'mention' },
|
|
attrs: {
|
|
userId: '123',
|
|
userFullName: 'John Doe',
|
|
mentionType: 'user',
|
|
},
|
|
},
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
});
|
|
|
|
it('creates a team mention node with correct attributes', () => {
|
|
const teamContent = {
|
|
id: '456',
|
|
name: 'Support Team',
|
|
type: 'team',
|
|
};
|
|
|
|
const result = getContentNode(mockEditorView, 'mention', teamContent, {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(
|
|
mockEditorView.state.schema.nodes.mention.create
|
|
).toHaveBeenCalledWith({
|
|
userId: '456',
|
|
userFullName: 'Support Team',
|
|
mentionType: 'team',
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
node: {
|
|
type: { name: 'mention' },
|
|
attrs: {
|
|
userId: '456',
|
|
userFullName: 'Support Team',
|
|
mentionType: 'team',
|
|
},
|
|
},
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
});
|
|
|
|
it('defaults to user mention type when type is not specified', () => {
|
|
const contentWithoutType = {
|
|
id: '789',
|
|
name: 'Jane Smith',
|
|
};
|
|
|
|
getContentNode(mockEditorView, 'mention', contentWithoutType, {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(
|
|
mockEditorView.state.schema.nodes.mention.create
|
|
).toHaveBeenCalledWith({
|
|
userId: '789',
|
|
userFullName: 'Jane Smith',
|
|
mentionType: 'user',
|
|
});
|
|
});
|
|
|
|
it('uses displayName over name when both are provided', () => {
|
|
const contentWithDisplayName = {
|
|
id: '101',
|
|
name: 'john_doe',
|
|
displayName: 'John Doe (Admin)',
|
|
type: 'user',
|
|
};
|
|
|
|
getContentNode(mockEditorView, 'mention', contentWithDisplayName, {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(
|
|
mockEditorView.state.schema.nodes.mention.create
|
|
).toHaveBeenCalledWith({
|
|
userId: '101',
|
|
userFullName: 'John Doe (Admin)',
|
|
mentionType: 'user',
|
|
});
|
|
});
|
|
|
|
it('handles missing displayName by falling back to name', () => {
|
|
const contentWithoutDisplayName = {
|
|
id: '102',
|
|
name: 'jane_smith',
|
|
type: 'user',
|
|
};
|
|
|
|
getContentNode(mockEditorView, 'mention', contentWithoutDisplayName, {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(
|
|
mockEditorView.state.schema.nodes.mention.create
|
|
).toHaveBeenCalledWith({
|
|
userId: '102',
|
|
userFullName: 'jane_smith',
|
|
mentionType: 'user',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('unsupported node types', () => {
|
|
it('returns null node for unsupported type', () => {
|
|
const result = getContentNode(mockEditorView, 'unsupported', 'content', {
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
node: null,
|
|
from: 0,
|
|
to: 5,
|
|
});
|
|
});
|
|
});
|
|
});
|