Skip to content
Open
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
12 changes: 12 additions & 0 deletions packages/markups/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,16 @@ module.exports = {
'@babel/preset-react',
'@emotion/babel-preset-css-prop',
],
env: {
test: {
presets: [
[
'@babel/preset-env',
{
modules: 'commonjs',
},
],
],
},
},
Comment on lines +14 to +25
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

env.test.presets adds a second @babel/preset-env on top of the top-level presets, so preset-env will run twice with different modules settings. To keep test/build transforms simpler and more predictable, consider conditionally configuring the existing preset-env (e.g., function export + api.env('test')) or using @babel/plugin-transform-modules-commonjs for tests instead of introducing a second preset-env.

Copilot uses AI. Check for mistakes.
};
6 changes: 6 additions & 0 deletions packages/markups/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
};
1 change: 1 addition & 0 deletions packages/markups/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"prebuild": "rimraf dist",
"build": "rollup -c --context=window --environment NODE_ENV:production",
"watch": "rollup -c --watch --context=window",
"test": "jest",
"test:lint": "eslint src/**/*.js",
"format": "prettier --write 'src/' ",
"format:check": "prettier --check 'src/' ",
Expand Down
16 changes: 16 additions & 0 deletions packages/markups/src/elements/InlineElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ import LinkSpan from './LinkSpan';
import UserMention from '../mentions/UserMention';
import TimestampElement from './TimestampElement';

const isPlainTextToken = (token) => token?.type === 'PLAIN_TEXT';

const isWrappedMention = (items, index) => {
const previous = items[index - 1];
const next = items[index + 1];

if (!isPlainTextToken(previous) || !isPlainTextToken(next)) {
return false;
}

return /\(\s*$/.test(previous.value) && /^\s*\)/.test(next.value);
};

const InlineElements = ({ contents }) =>
contents.map((content, index) => {
switch (content.type) {
Expand All @@ -34,6 +47,9 @@ const InlineElements = ({ contents }) =>
return <ChannelMention key={index} mention={content.value.value} />;

case 'MENTION_USER':
if (isWrappedMention(contents, index)) {
return <PlainSpan key={index} contents={`@${content.value.value}`} />;
}
return <UserMention key={index} contents={content.value} />;

case 'EMOJI':
Expand Down
93 changes: 93 additions & 0 deletions packages/markups/src/elements/InlineElements.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import InlineElements from './InlineElements';

jest.mock('./ItalicSpan', () => ({
__esModule: true,
default: () => <span data-kind="italic" />,
}));
jest.mock('./StrikeSpan', () => ({
__esModule: true,
default: () => <span data-kind="strike" />,
}));
jest.mock('./BoldSpan', () => ({
__esModule: true,
default: () => <span data-kind="bold" />,
}));
jest.mock('./CodeElement', () => ({
__esModule: true,
default: () => <span data-kind="inline-code" />,
}));
jest.mock('./Emoji', () => ({
__esModule: true,
default: () => <span data-kind="emoji" />,
}));
jest.mock('../mentions/ChannelMention', () => ({
__esModule: true,
default: () => <span data-kind="mention-channel" />,
}));
jest.mock('./ColorElement', () => ({
__esModule: true,
default: () => <span data-kind="color" />,
}));
jest.mock('./LinkSpan', () => ({
__esModule: true,
default: () => <span data-kind="link" />,
}));
jest.mock('./TimestampElement', () => ({
__esModule: true,
default: () => <span data-kind="timestamp" />,
}));

jest.mock('./PlainSpan', () => ({
__esModule: true,
default: ({ contents }) => <span data-kind="plain">{contents}</span>,
}));

jest.mock('../mentions/UserMention', () => ({
__esModule: true,
default: ({ contents }) => (
<span data-kind="mention-user">{contents.value}</span>
),
}));

describe('InlineElements mention rendering', () => {
test('renders wrapped user mentions as plain text', () => {
const contents = [
{ type: 'PLAIN_TEXT', value: '(' },
{ type: 'MENTION_USER', value: { type: 'PLAIN_TEXT', value: 'test' } },
{ type: 'PLAIN_TEXT', value: ')' },
];

const html = renderToStaticMarkup(<InlineElements contents={contents} />);

expect(html).toContain('data-kind="plain">@test</span>');
expect(html).not.toContain('data-kind="mention-user">test</span>');
});

test('renders wrapped mentions with spaces as plain text', () => {
const contents = [
{ type: 'PLAIN_TEXT', value: '( ' },
{ type: 'MENTION_USER', value: { type: 'PLAIN_TEXT', value: 'test' } },
{ type: 'PLAIN_TEXT', value: ' )' },
];

const html = renderToStaticMarkup(<InlineElements contents={contents} />);

expect(html).toContain('data-kind="plain">@test</span>');
expect(html).not.toContain('data-kind="mention-user">test</span>');
});

test('keeps normal mentions styled when not wrapped', () => {
const contents = [
{ type: 'PLAIN_TEXT', value: 'hello ' },
{ type: 'MENTION_USER', value: { type: 'PLAIN_TEXT', value: 'test' } },
{ type: 'PLAIN_TEXT', value: ' world' },
];

const html = renderToStaticMarkup(<InlineElements contents={contents} />);

expect(html).toContain('data-kind="mention-user">test</span>');
expect(html).not.toContain('data-kind="plain">@test</span>');
});
});
Loading