Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* eslint-disable */

Check failure on line 1 in packages/react-ui/src/app/features/builder/block-properties/text-input-with-mentions/tests/text-input-with-mentions.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Specify the rules you want to disable.

See more on https://sonarcloud.io/project/issues?id=openops-cloud_openops&issues=AZ47W1wt3zvopZWNom40&open=AZ47W1wt3zvopZWNom40&pullRequest=2299

import '@testing-library/jest-dom';
import { render, waitFor } from '@testing-library/react';
import { TextInputWithMentions } from '../index';

const mockSetInsertMentionHandler = jest.fn();

jest.mock('@/app/features/builder/builder-hooks', () => ({
useBuilderStateContext: (selector: (state: unknown) => unknown) =>
selector({
flowVersion: {
trigger: {
name: 'trigger',
displayName: 'Trigger',
type: 'TRIGGER',
settings: {},
valid: true,
nextAction: {
name: 'step_a',
displayName: 'Get EC2 Instances',
type: 'BLOCK',
settings: {},
valid: true,
},
},
},
setInsertMentionHandler: mockSetInsertMentionHandler,
}),
}));

jest.mock('@/app/features/blocks/lib/blocks-hook', () => ({
blocksHooks: {
useStepsMetadata: (steps: unknown[]) =>
steps.map((step: unknown, index: number) => ({
Comment thread
alexandrudanpop marked this conversation as resolved.
data: {
displayName: (step as { displayName: string }).displayName,
logoUrl: 'https://example.com/logo.png',
stepDisplayName: (step as { displayName: string }).displayName,
},
})),
},
}));

describe('TextInputWithMentions', () => {
it('renders without crashing with plain text', async () => {
const onChange = jest.fn();
const { container } = render(
<TextInputWithMentions
initialValue="hello world"
onChange={onChange}
placeholder="Enter value"
/>,
);

await waitFor(() => {
expect(container.querySelector('.tiptap')).toBeInTheDocument();
});
});

it('renders without crashing when initial value contains mention syntax', async () => {
const onChange = jest.fn();

// This is the key regression test: if renderHTML returns an invalid
// DOMOutputSpec (e.g. a DOM Element instead of an array), ProseMirror
// will throw "RangeError: Invalid array passed to renderSpec"
expect(() => {
render(
<TextInputWithMentions
initialValue="{{trigger.body}}"
onChange={onChange}
/>,
);
}).not.toThrow();
});

it('renders mention nodes as spans with correct attributes', async () => {
const onChange = jest.fn();
const { container } = render(
<TextInputWithMentions
initialValue="{{trigger.body}}"
onChange={onChange}
/>,
);

await waitFor(() => {
const mention = container.querySelector('[data-type="mention"]');
expect(mention).toBeInTheDocument();
expect(mention?.tagName.toLowerCase()).toBe('span');
expect(mention).toHaveAttribute('contenteditable', 'false');
});
});

it('renders multiple mentions without errors', async () => {
const onChange = jest.fn();

expect(() => {
render(
<TextInputWithMentions
initialValue="{{trigger.body}} and {{step_a.output}}"
onChange={onChange}
/>,
);
}).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MentionNodeAttrs } from '@tiptap/extension-mention';
import { DOMOutputSpec } from '@tiptap/pm/model';
import { JSONContent } from '@tiptap/react';

import { StepMetadata } from '@openops/components/ui';
Expand Down Expand Up @@ -190,39 +191,40 @@ function convertTiptapJsonToText(nodes: JSONContent[]): string {
return res.join('');
}

const generateMentionHtmlElement = (mentionAttrs: MentionNodeAttrs) => {
const mentionElement = document.createElement('span');
const generateMentionHtmlElement = (
mentionAttrs: MentionNodeAttrs,
): DOMOutputSpec => {
const apMentionNodeAttrs: MentionNodeAttrs = JSON.parse(
mentionAttrs.label || '{}',
);
mentionElement.className =
'inline-flex bg-muted/10 break-all my-1 mx-[1px] border border-[#9e9e9e] border-solid items-center gap-2 py-1 px-2 rounded-[3px] text-muted-foreground ';
assertNotNullOrUndefined(mentionAttrs.label, 'mentionAttrs.label');
assertNotNullOrUndefined(mentionAttrs.id, 'mentionAttrs.id');
assertNotNullOrUndefined(
apMentionNodeAttrs.displayText,
'apMentionNodeAttrs.displayText',
);
mentionElement.dataset.id = mentionAttrs.id;
mentionElement.dataset.label = mentionAttrs.label;
mentionElement.dataset.displayText = apMentionNodeAttrs.displayText;
mentionElement.dataset.type = TipTapNodeTypes.mention;
mentionElement.contentEditable = 'false';

const attrs: Record<string, string> = {
class:
'inline-flex bg-muted/10 break-all my-1 mx-[1px] border border-[#9e9e9e] border-solid items-center gap-2 py-1 px-2 rounded-[3px] text-muted-foreground',
'data-id': mentionAttrs.id,
'data-label': mentionAttrs.label,
'data-display-text': apMentionNodeAttrs.displayText,
'data-type': TipTapNodeTypes.mention,
contenteditable: 'false',
servervalue: apMentionNodeAttrs.serverValue,
};

const children: DOMOutputSpec[] = [];
if (apMentionNodeAttrs.logoUrl) {
const imgElement = document.createElement('img');
imgElement.src = apMentionNodeAttrs.logoUrl;
imgElement.className = 'object-fit w-4 h-4';
mentionElement.appendChild(imgElement);
children.push([
'img',
{ src: apMentionNodeAttrs.logoUrl, class: 'object-fit w-4 h-4' },
Comment thread
alexandrudanpop marked this conversation as resolved.
]);
}
children.push(apMentionNodeAttrs.displayText);

const mentiontextDiv = document.createTextNode(
apMentionNodeAttrs.displayText,
);
mentionElement.setAttribute('serverValue', apMentionNodeAttrs.serverValue);

mentionElement.appendChild(mentiontextDiv);
return mentionElement;
return ['span', attrs, ...children];
};

const inputThatUsesMentionClass = 'ap-text-with-mentions';
Expand Down
Loading