mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 18:08:54 +00:00
* Add phone number matching helpers for tel: autolinks. Detect E.164, grouped national numbers, and tel: URIs while skipping bare digit runs that look like ticket IDs or dates. Co-authored-by: joram <joram@mattermost.com> * Autolink phone numbers in post markdown as tel: links. Wrap recognized numbers in markdown link nodes after parsing so tapping opens the device dialer. Skip code, existing links, and unsafe posts. Co-authored-by: joram <joram@mattermost.com> * Copy phone numbers from tel: markdown links on long-press. Treat tel: like mailto: in link and channel-header sheets, and cover opening tel: URLs through the existing openLink path. Co-authored-by: joram <joram@mattermost.com> * Reject truncated phone matches inside longer digit sequences. Skip past hyphens and dots when checking match boundaries so overlong numbers and numeric prefixes are not autolinked. Keep spaces and parentheses as boundaries so adjacent numbers still match. Also address CodeRabbit test nits: assert match counts, prefix autolinkPhoneNumbers cases with should, and assert rendered tel: links. Co-authored-by: joram <joram@mattermost.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {screen} from '@testing-library/react-native';
|
|
import * as CommonMark from 'commonmark';
|
|
const {Node} = CommonMark;
|
|
import React from 'react';
|
|
import {StyleSheet} from 'react-native';
|
|
|
|
import {Preferences, Screens} from '@constants';
|
|
import {renderWithIntl} from '@test/intl-test-helper';
|
|
import {getMarkdownTextStyles} from '@utils/markdown';
|
|
import {typography} from '@utils/typography';
|
|
|
|
import Markdown, {testExports} from './markdown';
|
|
import * as Transforms from './transform';
|
|
|
|
const Parser = jest.requireActual('commonmark').Parser;
|
|
|
|
jest.mock('@screens/navigation', () => ({
|
|
navigateToRoot: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('./markdown_link', () => {
|
|
const MockReact = require('react');
|
|
const {Text} = require('react-native');
|
|
|
|
return {
|
|
__esModule: true,
|
|
default: ({href, children}: {href: string; children: React.ReactNode}) => (
|
|
MockReact.createElement(Text, {testID: 'markdown_link', href}, children)
|
|
),
|
|
};
|
|
});
|
|
|
|
describe('Markdown', () => {
|
|
const baseProps: React.ComponentProps<typeof Markdown> = {
|
|
baseTextStyle: {},
|
|
enableInlineLatex: true,
|
|
enableLatex: true,
|
|
location: Screens.CHANNEL,
|
|
maxNodes: 2000,
|
|
theme: Preferences.THEMES.denim,
|
|
};
|
|
|
|
describe('error handling', () => {
|
|
test('should render Markdown normally', () => {
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='This is a test'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByText('This is a test')).toBeVisible();
|
|
});
|
|
|
|
test('should catch errors when parsing Markdown', () => {
|
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
|
const parser = new Parser();
|
|
parser.incorporateLine = () => {
|
|
throw new Error('test error');
|
|
};
|
|
return parser;
|
|
});
|
|
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='This is a test'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
|
});
|
|
|
|
test('should catch errors when parsing Markdown', () => {
|
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
|
const parser = new Parser();
|
|
parser.incorporateLine = () => {
|
|
throw new Error('test error');
|
|
};
|
|
return parser;
|
|
});
|
|
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='This is a test'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
|
});
|
|
|
|
test('should catch errors when transforming Markdown', () => {
|
|
jest.spyOn(Transforms, 'highlightMentions').mockImplementation(() => {
|
|
throw new Error('test error');
|
|
});
|
|
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='This is a test'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
|
expect(screen.getByText('An error occurred while parsing this text')).toBeVisible();
|
|
});
|
|
|
|
test('should catch errors when rendering Markdown', () => {
|
|
jest.spyOn(CommonMark, 'Parser').mockImplementation(() => {
|
|
const parser = new Parser();
|
|
parser.parse = () => {
|
|
// This replicates what was returned by the parser to cause MM-61148
|
|
const document = new Node('document');
|
|
|
|
const paragraph = new Node('paragraph');
|
|
(paragraph as any)._string_content = 'some text';
|
|
|
|
document.appendChild(new Node('table'));
|
|
|
|
return document;
|
|
};
|
|
return parser;
|
|
});
|
|
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='This is a test'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
|
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
|
});
|
|
|
|
test('should catch errors when with Latex', () => {
|
|
const value =
|
|
'```latex\n' +
|
|
'\\\\\\\\asdfasdfasdf\n' +
|
|
'```\n';
|
|
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value={value}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryByText('This is a text')).not.toBeVisible();
|
|
expect(screen.getByText('An error occurred while rendering this text')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe('renderHashtagWithStyles', () => {
|
|
const {renderHashtagWithStyles} = testExports;
|
|
|
|
test('should render hashtag with correct text and link styling', () => {
|
|
const baseTextStyle = {
|
|
color: '#000000',
|
|
...typography('Body', 200),
|
|
};
|
|
const textStyles = getMarkdownTextStyles(baseProps.theme);
|
|
const context: string[] = [];
|
|
|
|
renderWithIntl(
|
|
renderHashtagWithStyles(context, 'test', textStyles, baseTextStyle),
|
|
);
|
|
|
|
const hashtagElement = screen.getByText('#test');
|
|
expect(hashtagElement).toBeVisible();
|
|
|
|
const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style);
|
|
expect(flattenedStyle).toMatchObject({
|
|
fontSize: 16,
|
|
color: baseProps.theme.linkColor,
|
|
});
|
|
});
|
|
|
|
test('should render hashtag in heading with correct heading font size', () => {
|
|
const baseTextStyle = {
|
|
color: '#000000',
|
|
...typography('Body', 200),
|
|
};
|
|
const textStyles = getMarkdownTextStyles(baseProps.theme);
|
|
const context: string[] = ['heading1'];
|
|
|
|
renderWithIntl(
|
|
renderHashtagWithStyles(context, 'hashtag', textStyles, baseTextStyle),
|
|
);
|
|
|
|
const hashtagElement = screen.getByText('#hashtag');
|
|
expect(hashtagElement).toBeVisible();
|
|
|
|
const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style);
|
|
expect(flattenedStyle).toMatchObject({
|
|
fontSize: 28,
|
|
});
|
|
});
|
|
|
|
test('should apply heading2 styles when in heading2 context', () => {
|
|
const baseTextStyle = {
|
|
color: '#000000',
|
|
...typography('Body', 200),
|
|
};
|
|
const textStyles = getMarkdownTextStyles(baseProps.theme);
|
|
const context: string[] = ['heading2'];
|
|
|
|
renderWithIntl(
|
|
renderHashtagWithStyles(context, 'heading2tag', textStyles, baseTextStyle),
|
|
);
|
|
|
|
const hashtagElement = screen.getByText('#heading2tag');
|
|
expect(hashtagElement).toBeVisible();
|
|
|
|
const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style);
|
|
expect(flattenedStyle).toMatchObject({
|
|
fontSize: 25,
|
|
});
|
|
});
|
|
|
|
test('should have clickable onPress handler', () => {
|
|
const baseTextStyle = {
|
|
color: '#000000',
|
|
...typography('Body', 200),
|
|
};
|
|
const textStyles = getMarkdownTextStyles(baseProps.theme);
|
|
const context: string[] = [];
|
|
|
|
renderWithIntl(
|
|
renderHashtagWithStyles(context, 'clickable', textStyles, baseTextStyle),
|
|
);
|
|
|
|
const hashtagElement = screen.getByText('#clickable');
|
|
expect(hashtagElement.props.onPress).toBeDefined();
|
|
});
|
|
|
|
test('should include both base text style and link color', () => {
|
|
const baseTextStyle = {
|
|
color: '#000000',
|
|
...typography('Body', 200),
|
|
fontFamily: 'OpenSans',
|
|
};
|
|
const textStyles = getMarkdownTextStyles(baseProps.theme);
|
|
const context: string[] = [];
|
|
|
|
renderWithIntl(
|
|
renderHashtagWithStyles(context, 'styled', textStyles, baseTextStyle),
|
|
);
|
|
|
|
const hashtagElement = screen.getByText('#styled');
|
|
const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style);
|
|
|
|
expect(flattenedStyle).toMatchObject({
|
|
fontSize: 16,
|
|
fontFamily: 'OpenSans',
|
|
color: baseProps.theme.linkColor,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('phone number autolinking', () => {
|
|
beforeEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('should autolink phone numbers when links are enabled', () => {
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
value='Call 555-123-4567'
|
|
/>,
|
|
);
|
|
|
|
const links = screen.getAllByTestId('markdown_link');
|
|
expect(links).toHaveLength(1);
|
|
expect(links[0].props.href).toBe('tel:5551234567');
|
|
expect(screen.getByText('555-123-4567')).toBeVisible();
|
|
});
|
|
|
|
it('should not autolink phone numbers when links are disabled', () => {
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
disableLinks={true}
|
|
value='Call 555-123-4567'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryAllByTestId('markdown_link')).toHaveLength(0);
|
|
expect(screen.getByText('Call 555-123-4567')).toBeVisible();
|
|
});
|
|
|
|
it('should not autolink phone numbers when the post has unsafe links', () => {
|
|
renderWithIntl(
|
|
<Markdown
|
|
{...baseProps}
|
|
isUnsafeLinksPost={true}
|
|
value='Call 555-123-4567'
|
|
/>,
|
|
);
|
|
|
|
expect(screen.queryAllByTestId('markdown_link')).toHaveLength(0);
|
|
expect(screen.getByText('Call 555-123-4567')).toBeVisible();
|
|
});
|
|
});
|
|
});
|