* Improve markdown code * Add missing changes * Simplify style composition * Address feedback * Fix color issue introduced by channel banner
241 lines
9.7 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|