From 5261e29c38e37a794e600400333fcd0c79f76162 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 12 Aug 2024 09:16:11 +0300 Subject: [PATCH] add unit tests to app/utils/markdown (#8135) --- app/utils/markdown/index.test.ts | 203 +++++++++++++++++++++++++++++++ app/utils/markdown/index.ts | 7 +- 2 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 app/utils/markdown/index.test.ts diff --git a/app/utils/markdown/index.test.ts b/app/utils/markdown/index.test.ts new file mode 100644 index 000000000..63f286851 --- /dev/null +++ b/app/utils/markdown/index.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Platform, type TextStyle} from 'react-native'; + +import {Preferences} from '@constants'; + +import { + getCodeFont, + getMarkdownTextStyles, + getMarkdownBlockStyles, + getHighlightLanguageFromNameOrAlias, + getHighlightLanguageName, + escapeRegex, + getMarkdownImageSize, + computeTextStyle, + parseSearchTerms, + convertSearchTermToRegex, +} 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(computeTextStyle(textStyles, baseStyle, ['bold'])).toEqual([baseStyle, textStyles.bold]); + }); + + it('should apply multiple context styles', () => { + expect(computeTextStyle(textStyles, baseStyle, ['bold', 'italic'])).toEqual([baseStyle, textStyles.bold, textStyles.italic]); + }); + + it('should ignore undefined styles', () => { + expect(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 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); + }); + }); +}); diff --git a/app/utils/markdown/index.ts b/app/utils/markdown/index.ts index 9611c3f6c..95824ffcd 100644 --- a/app/utils/markdown/index.ts +++ b/app/utils/markdown/index.ts @@ -331,10 +331,11 @@ export function parseSearchTerms(searchTerm: string): string[] | undefined { } // remove punctuation from each term - terms = terms.map((term) => { - term.replace(puncStart, ''); + terms = terms.map((t) => { + let term = t; + term = term.replace(puncStart, ''); if (term.charAt(term.length - 1) !== '*') { - term.replace(puncEnd, ''); + term = term.replace(puncEnd, ''); } return term; });