mattermost-mobile/app/utils/markdown/index.test.ts
Daniel Espino García 1d8568ade8
Improve markdown code (#9164)
* Improve markdown code

* Add missing changes

* Simplify style composition

* Address feedback

* Fix color issue introduced by channel banner
2025-11-04 10:05:56 +01:00

241 lines
9.7 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform, StyleSheet, type TextStyle} from 'react-native';
import {Preferences} from '@constants';
import {
getCodeFont,
getMarkdownTextStyles,
getMarkdownBlockStyles,
getHighlightLanguageFromNameOrAlias,
getHighlightLanguageName,
escapeRegex,
getMarkdownImageSize,
computeTextStyle,
parseSearchTerms,
convertSearchTermToRegex,
removeImageProxyForKey,
} from './index';
jest.mock('@utils/images', () => ({
getViewPortWidth: jest.fn(),
}));
jest.mock('@utils/log', () => ({
logError: jest.fn(),
}));
describe('Utility functions', () => {
describe('getCodeFont', () => {
it('should return the correct font for iOS', () => {
Platform.OS = 'ios';
expect(getCodeFont()).toBe('Menlo');
});
it('should return the correct font for Android', () => {
Platform.OS = 'android';
expect(getCodeFont()).toBe('monospace');
});
});
describe('getMarkdownTextStyles', () => {
it('should return correct text styles', () => {
const styles = getMarkdownTextStyles(Preferences.THEMES.denim);
expect(styles).toHaveProperty('emph');
expect(styles).toHaveProperty('strong');
expect(styles).toHaveProperty('del');
expect(styles).toHaveProperty('link');
expect(styles).toHaveProperty('heading1');
expect(styles).toHaveProperty('code');
expect(styles).toHaveProperty('mention');
expect(styles).toHaveProperty('error');
expect(styles).toHaveProperty('table_header_row');
expect(styles).toHaveProperty('mention_highlight');
});
});
describe('getMarkdownBlockStyles', () => {
it('should return correct block styles', () => {
const styles = getMarkdownBlockStyles(Preferences.THEMES.denim);
expect(styles).toHaveProperty('adjacentParagraph');
expect(styles).toHaveProperty('horizontalRule');
expect(styles).toHaveProperty('quoteBlockIcon');
});
});
describe('getHighlightLanguageFromNameOrAlias', () => {
it('should return correct language name or alias', () => {
expect(getHighlightLanguageFromNameOrAlias('javascript')).toBe('javascript');
expect(getHighlightLanguageFromNameOrAlias('js')).toBe('javascript');
expect(getHighlightLanguageFromNameOrAlias('unknown')).toBe('');
});
});
describe('getHighlightLanguageName', () => {
it('should return correct language name', () => {
expect(getHighlightLanguageName('javascript')).toBe('JavaScript');
expect(getHighlightLanguageName('unknown')).toBe('');
});
});
describe('escapeRegex', () => {
it('should escape special regex characters', () => {
expect(escapeRegex('hello.*')).toBe('hello\\.\\*');
});
});
describe('getMarkdownImageSize', () => {
it('should return correct size for sourceSize', () => {
const size = getMarkdownImageSize(false, false, {width: 100, height: 200});
expect(size).toEqual({width: 100, height: 200});
});
it('should return correct size for sourceSize without height', () => {
const size = getMarkdownImageSize(false, false, {width: 100});
expect(size).toEqual({width: 100, height: 100});
});
it('should return correct size for sourceSize without height and known size', () => {
const size = getMarkdownImageSize(false, false, {width: 100}, {width: 100, height: 200});
expect(size).toEqual({width: 100, height: 200});
});
it('should return correct size for sourceSize height height and known size', () => {
const size = getMarkdownImageSize(false, false, {height: 100}, {width: 100, height: 200});
expect(size).toEqual({width: 50, height: 100});
});
it('should return correct size for knownSize', () => {
const size = getMarkdownImageSize(false, false, undefined, {width: 100, height: 200});
expect(size).toEqual({width: 100, height: 200});
});
it('should return correct size when no metadata and source size is not specified', () => {
const size = getMarkdownImageSize(false, false, undefined, undefined, 150, 250);
expect(size).toEqual({width: 150, height: 250});
});
});
describe('computeTextStyle', () => {
const textStyles: { [key: string]: TextStyle } = {
bold: {fontWeight: 'bold'},
italic: {fontStyle: 'italic'},
underline: {textDecorationLine: 'underline'},
};
const baseStyle: TextStyle = {color: 'black'};
it('should return base style if context is empty', () => {
expect(computeTextStyle(textStyles, baseStyle, [])).toEqual(baseStyle);
});
it('should return base style if context has no matching styles', () => {
expect(computeTextStyle(textStyles, baseStyle, ['unknown'])).toEqual(baseStyle);
});
it('should apply a single context style', () => {
expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold']))).toEqual({...baseStyle, ...textStyles.bold});
});
it('should apply multiple context styles', () => {
expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold', 'italic']))).toEqual({...baseStyle, ...textStyles.bold, ...textStyles.italic});
});
it('should ignore undefined styles', () => {
expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold', 'unknown']))).toEqual({...baseStyle, ...textStyles.bold});
});
it('should handle multiple undefined styles', () => {
expect(computeTextStyle(textStyles, baseStyle, ['unknown1', 'unknown2'])).toEqual(baseStyle);
});
});
describe('parseSearchTerms', () => {
it('should capture quoted strings', () => {
expect(parseSearchTerms('"hello world"')).toEqual(['hello world']);
});
it('should ignore search flags', () => {
expect(parseSearchTerms('in:channel before:2021-01-01')).toEqual([]);
});
it('should capture @ mentions', () => {
expect(parseSearchTerms('@username')).toEqual(['username']);
});
it('should capture plain text up to the next quote or search flag', () => {
expect(parseSearchTerms('plain text "quoted text"')).toEqual(['plain', 'text', 'quoted text']);
});
it('should split plain text into words', () => {
expect(parseSearchTerms('this is a test')).toEqual(['this', 'is', 'a', 'test']);
});
it('should handle a mix of all cases', () => {
const searchTerm = 'in:channel @username "quoted text" plain text';
const expected = ['username', 'quoted text', 'plain', 'text'];
expect(parseSearchTerms(searchTerm)).toEqual(expected);
});
});
describe('convertSearchTermToRegex', () => {
it('should create regex for CJK characters', () => {
const result = convertSearchTermToRegex('你好');
expect(result.pattern).toEqual(/()(你好)/gi);
});
it('should create regex for Thai characters', () => {
const result = convertSearchTermToRegex('สวัสดี');
expect(result.pattern).toEqual(/()(สวัสดี)/gi);
});
it('should create regex for wildcard at the end', () => {
const result = convertSearchTermToRegex('hello*');
expect(result.pattern).toEqual(/\b()(hello)/gi);
});
it('should create regex for mentions and hashtags', () => {
const result = convertSearchTermToRegex('@user');
expect(result.pattern).toEqual(/(\W|^)(@user)\b/gi);
});
it('should create regex for plain text', () => {
const result = convertSearchTermToRegex('hello');
expect(result.pattern).toEqual(/\b()(hello)\b/gi);
});
});
describe('removeImageProxyForKey', () => {
test('should return the original URL if the key contains a valid proxy URL with a query parameter', () => {
const proxyKey = 'https://proxy.example.com/image?url=https%3A%2F%2Foriginal.example.com%2Fimage.png';
const result = removeImageProxyForKey(proxyKey);
expect(result).toBe('https://original.example.com/image.png');
});
test('should return the key itself if it does not contain a query parameter', () => {
const key = 'https://proxy.example.com/image';
const result = removeImageProxyForKey(key);
expect(result).toBe(key);
});
test('should return the key itself if the query parameter does not contain a URL', () => {
const key = 'https://proxy.example.com/image?otherParam=value';
const result = removeImageProxyForKey(key);
expect(result).toBe(key);
});
test('should decode the URL if it is encoded in the query parameter', () => {
const proxyKey = 'https://proxy.example.com/image?url=https%3A%2F%2Foriginal.example.com%2Fpath%2Fto%2Fimage%3Fparam%3Dvalue';
const result = removeImageProxyForKey(proxyKey);
expect(result).toBe('https://original.example.com/path/to/image?param=value');
});
test('should handle keys with no query string gracefully', () => {
const key = 'https://proxy.example.com/image';
const result = removeImageProxyForKey(key);
expect(result).toBe(key);
});
});
});