import { splitTextToChars, splitLineToFitWidth, splitLineToWords } from './splitText.js'; import { describe, it, expect, vi } from 'vitest'; import type { CheckFitFunction, MarkdownLine, MarkdownWordType } from './types.js'; describe('when Intl.Segmenter is available', () => { describe('splitText', () => { it.each([ { str: '', split: [] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'ok', split: ['o', 'k'] }, { str: 'abc', split: ['a', 'b', 'c'] }, ])('should split $str into graphemes', ({ str, split }: { str: string; split: string[] }) => { expect(splitTextToChars(str)).toEqual(split); }); }); describe('split lines', () => { it('should create valid checkFit function', () => { const checkFit5 = createCheckFn(5); expect(checkFit5([{ content: 'hello', type: 'normal' }])).toBe(true); expect( checkFit5([ { content: 'hello', type: 'normal' }, { content: 'world', type: 'normal' }, ]) ).toBe(false); const checkFit1 = createCheckFn(1); expect(checkFit1([{ content: 'A', type: 'normal' }])).toBe(true); expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(true); expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(false); }); it.each([ // empty string { str: 'hello world', width: 7, split: ['hello', 'world'] }, // width > full line { str: 'hello world', width: 20, split: ['hello world'] }, // width < individual word { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, // width = 0, impossible, so split into individual characters { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 0, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 2, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 3, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'δΈ­ζ–‡δΈ­', width: 1, split: ['δΈ­', 'ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 2, split: ['δΈ­ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 3, split: ['δΈ­ζ–‡δΈ­'] }, { str: 'Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ', width: 6, split: ['Flag πŸ³οΈβ€βš§οΈ', 'this πŸ³οΈβ€πŸŒˆ'] }, ])( 'should split $str into lines of $width characters', ({ str, split, width }: { str: string; width: number; split: string[] }) => { const checkFn = createCheckFn(width); const line: MarkdownLine = getLineFromString(str); expect(splitLineToFitWidth(line, checkFn)).toEqual( split.map((str) => getLineFromString(str)) ); } ); }); }); /** * Intl.Segmenter is not supported in Firefox yet, * see https://bugzilla.mozilla.org/show_bug.cgi?id=1423593 */ describe('when Intl.Segmenter is not available', () => { beforeAll(() => { vi.stubGlobal('Intl', { Segmenter: undefined }); }); afterAll(() => { vi.unstubAllGlobals(); }); it.each([ { str: '', split: [] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: [...'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'], }, { str: 'ok', split: ['o', 'k'] }, { str: 'abc', split: ['a', 'b', 'c'] }, ])('should split $str into characters', ({ str, split }: { str: string; split: string[] }) => { expect(splitTextToChars(str)).toEqual(split); }); it.each([ // empty string { str: 'hello world', width: 7, split: ['hello', 'world'] }, // width > full line { str: 'hello world', width: 20, split: ['hello world'] }, // width < individual word { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, // width = 0, impossible, so split into individual characters { str: 'abc', width: 0, split: ['a', 'b', 'c'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: [...'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'δΈ­ζ–‡δΈ­', width: 1, split: ['δΈ­', 'ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 2, split: ['δΈ­ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 3, split: ['δΈ­ζ–‡δΈ­'] }, ])( 'should split $str into lines of $width characters', ({ str, split, width }: { str: string; width: number; split: string[] }) => { const checkFn = createCheckFn(width); const line: MarkdownLine = getLineFromString(str); expect(splitLineToFitWidth(line, checkFn)).toEqual( split.map((str) => getLineFromString(str)) ); } ); }); it('should handle strings with newlines', () => { const checkFn: CheckFitFunction = createCheckFn(6); const str = `Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ`; expect(() => splitLineToFitWidth(getLineFromString(str), checkFn) ).toThrowErrorMatchingInlineSnapshot( `[Error: splitLineToFitWidth does not support newlines in the line]` ); }); const getLineFromString = (str: string, type: MarkdownWordType = 'normal'): MarkdownLine => { return splitLineToWords(str).map((content) => ({ content, type, })); }; /** * Creates a checkFunction for a given width * @param width - width of characters to fit in a line * @returns checkFunction */ const createCheckFn = (width: number): CheckFitFunction => { return (text: MarkdownLine) => { // Join all words into a single string const joinedContent = text.map((w) => w.content).join(''); const characters = splitTextToChars(joinedContent); return characters.length <= width; }; }; // cspell:ignore worl