diff options
Diffstat (limited to 'packages/excalidraw/element/textWrapping.test.ts')
| -rw-r--r-- | packages/excalidraw/element/textWrapping.test.ts | 633 |
1 files changed, 633 insertions, 0 deletions
diff --git a/packages/excalidraw/element/textWrapping.test.ts b/packages/excalidraw/element/textWrapping.test.ts new file mode 100644 index 0000000..6c7bcb8 --- /dev/null +++ b/packages/excalidraw/element/textWrapping.test.ts @@ -0,0 +1,633 @@ +import { wrapText, parseTokens } from "./textWrapping"; +import type { FontString } from "./types"; + +describe("Test wrapText", () => { + // font is irrelevant as jsdom does not support FontFace API + // `measureText` width is mocked to return `text.length` by `jest-canvas-mock` + // https://github.com/hustcc/jest-canvas-mock/blob/master/src/classes/TextMetrics.js + const font = "10px Cascadia, Segoe UI Emoji" as FontString; + + it("should wrap the text correctly when word length is exactly equal to max width", () => { + const text = "Hello Excalidraw"; + // Length of "Excalidraw" is 100 and exacty equal to max width + const res = wrapText(text, font, 100); + expect(res).toEqual(`Hello\nExcalidraw`); + }); + + it("should return the text as is if max width is invalid", () => { + const text = "Hello Excalidraw"; + expect(wrapText(text, font, NaN)).toEqual(text); + expect(wrapText(text, font, -1)).toEqual(text); + expect(wrapText(text, font, Infinity)).toEqual(text); + }); + + it("should show the text correctly when max width reached", () => { + const text = "Helloπ"; + const maxWidth = 10; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("H\ne\nl\nl\no\nπ"); + }); + + it("should not wrap number when wrapping line", () => { + const text = "don't wrap this number 99,100.99"; + const maxWidth = 300; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("don't wrap this number\n99,100.99"); + }); + + it("should trim all trailing whitespaces", () => { + const text = "Hello "; + const maxWidth = 50; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello"); + }); + + it("should trim all but one trailing whitespaces", () => { + const text = "Hello "; + const maxWidth = 60; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello "); + }); + + it("should keep preceding whitespaces and trim all trailing whitespaces", () => { + const text = " Hello World"; + const maxWidth = 90; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(" Hello\nWorld"); + }); + + it("should keep some preceding whitespaces, trim trailing whitespaces, but kep those that fit in the trailing line", () => { + const text = " Hello World "; + const maxWidth = 90; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(" Hello\nWorld "); + }); + + it("should trim keep those whitespace that fit in the trailing line", () => { + const text = "Hello Wo rl d "; + const maxWidth = 100; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello Wo\nrl d "); + }); + + it("should support multiple (multi-codepoint) emojis", () => { + const text = "ππΊπ₯π©π½βπ¦°π¨βπ©βπ§βπ¦π¨πΏ"; + const maxWidth = 1; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("π\nπΊ\nπ₯\nπ©π½βπ¦°\nπ¨βπ©βπ§βπ¦\nπ¨πΏ"); + }); + + it("should wrap the text correctly when text contains hyphen", () => { + let text = + "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects"; + const res = wrapText(text, font, 110); + expect(res).toBe( + `Wikipedia\nis hosted\nby\nWikimedia-\nFoundation,\na non-\nprofit\norganizatio\nn that also\nhosts a\nrange-of\nother\nprojects`, + ); + + text = "Hello thereusing-now"; + expect(wrapText(text, font, 100)).toEqual("Hello\nthereusing\n-now"); + }); + + it("should support wrapping nested lists", () => { + const text = `\tA) one tab\t\t- two tabs - 8 spaces`; + + const maxWidth = 100; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(`\tA) one\ntab\t\t- two\ntabs\n- 8 spaces`); + + const maxWidth2 = 50; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe(`\tA)\none\ntab\n- two\ntabs\n- 8\nspace\ns`); + }); + + describe("When text is CJK", () => { + it("should break each CJK character when width is very small", () => { + // "μλ
νμΈμ" (Hangul) + "γγγ«γ‘γ―δΈη" (Hiragana, Kanji) + "ο½ΊοΎοΎοΎγ" (Katakana) + "δ½ ε₯½" (Han) = "Hello Hello World Hello Hi" + const text = "μλ
νμΈμγγγ«γ‘γ―δΈηο½ΊοΎοΎοΎγδ½ ε₯½"; + const maxWidth = 10; + const res = wrapText(text, font, maxWidth); + expect(res).toBe( + "μ\nλ
\nν\nμΈ\nμ\nγ\nγ\nγ«\nγ‘\nγ―\nδΈ\nη\nο½Ί\nοΎ\nοΎ\nοΎ\nγ\nδ½ \nε₯½", + ); + }); + + it("should break CJK text into longer segments when width is larger", () => { + // "μλ
νμΈμ" (Hangul) + "γγγ«γ‘γ―δΈη" (Hiragana, Kanji) + "ο½ΊοΎοΎοΎγ" (Katakana) + "δ½ ε₯½" (Han) = "Hello Hello World Hello Hi" + const text = "μλ
νμΈμγγγ«γ‘γ―δΈηο½ΊοΎοΎοΎγδ½ ε₯½"; + const maxWidth = 30; + const res = wrapText(text, font, maxWidth); + + // measureText is mocked, so it's not precisely what would happen in prod + expect(res).toBe("μλ
ν\nμΈμγ\nγγ«γ‘\nγ―δΈη\nο½ΊοΎοΎ\nοΎγδ½ \nε₯½"); + }); + + it("should handle a combination of CJK, latin, emojis and whitespaces", () => { + const text = `aι« ι« bb δ½ ε₯½ world-i-ππΊπ₯`; + + const maxWidth = 150; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(`aι« ι« bb δ½ \nε₯½ world-i-ππΊ\nπ₯`); + + const maxWidth2 = 50; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe(`aι« ι«\nbb δ½ \nε₯½\nworld\n-i-π\nπΊπ₯`); + + const maxWidth3 = 30; + const res3 = wrapText(text, font, maxWidth3); + expect(res3).toBe(`aι«\nι«\nbb\nδ½ ε₯½\nwor\nld-\ni-\nπ\nπΊ\nπ₯`); + }); + + it("should break before and after a regular CJK character", () => { + const text = "HelloγWorld"; + const maxWidth1 = 50; + const res1 = wrapText(text, font, maxWidth1); + expect(res1).toBe("Hello\nγ\nWorld"); + + const maxWidth2 = 60; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe("Helloγ\nWorld"); + }); + + it("should break before and after certain CJK symbols", () => { + const text = "γγγ«γ‘γ―γδΈη"; + const maxWidth1 = 50; + const res1 = wrapText(text, font, maxWidth1); + expect(res1).toBe("γγγ«γ‘γ―\nγδΈη"); + + const maxWidth2 = 60; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe("γγγ«γ‘γ―γ\nδΈη"); + }); + + it("should break after, not before for certain CJK pairs", () => { + const text = "Hello γγ"; + const maxWidth = 70; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello\nγγ"); + }); + + it("should break before, not after for certain CJK pairs", () => { + const text = "HelloγγWorldγ"; + const maxWidth = 60; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hello\nγγ\nWorldγ"); + }); + + it("should break after, not before for certain CJK character pairs", () => { + const text = "γHelloγγWorld"; + const maxWidth = 70; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("γHello\nγγWorld"); + }); + + it("should break Chinese sentences", () => { + const text = `δΈε½δ½ ε₯½οΌθΏζ―δΈδΈͺζ΅θ―γ +ζ们ζ₯ηηοΌδΊΊζ°εΈΒ₯1234γεΎθ΄΅γ +οΌζ¬ε·οΌγιε·οΌε₯ε·γη©Ίζ Ό ζ’θ‘γε
¨θ§η¬¦ε·β¦β`; + + const maxWidth1 = 80; + const res1 = wrapText(text, font, maxWidth1); + expect(res1).toBe(`δΈε½δ½ ε₯½οΌθΏζ―δΈ\nδΈͺζ΅θ―γ +ζ们ζ₯ηηοΌδΊΊζ°\nεΈΒ₯1234γεΎ\nθ΄΅γ +οΌζ¬ε·οΌγιε·οΌ\nε₯ε·γη©Ίζ Ό ζ’θ‘\nε
¨θ§η¬¦ε·β¦β`); + + const maxWidth2 = 50; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe(`δΈε½δ½ ε₯½οΌ\nθΏζ―δΈδΈͺζ΅\nθ―γ +ζ们ζ₯η\nηοΌδΊΊζ°εΈ\nΒ₯1234\nγεΎθ΄΅γ +οΌζ¬ε·οΌγ\nιε·οΌε₯\nε·γη©Ίζ Ό\nζ’θ‘γε
¨θ§\n符ε·β¦β`); + }); + + it("should break Japanese sentences", () => { + const text = `ζ₯ζ¬γγγ«γ‘γ―οΌγγγ―γγΉγγ§γγ + θ¦γ¦γΏγΎγγγοΌεοΏ₯1234γι«γγ + οΌζ¬εΌ§οΌγθͺηΉγε₯ηΉγ + η©Ίη½ ζΉθ‘γε
¨θ§θ¨ε·β¦γΌ`; + + const maxWidth1 = 80; + const res1 = wrapText(text, font, maxWidth1); + expect(res1).toBe(`ζ₯ζ¬γγγ«γ‘γ―οΌ\nγγγ―γγΉγγ§\nγγ + θ¦γ¦γΏγΎγγ\nγοΌεοΏ₯1234\nγι«γγ + οΌζ¬εΌ§οΌγθͺ\nηΉγε₯ηΉγ + η©Ίη½ ζΉθ‘\nε
¨θ§θ¨ε·β¦γΌ`); + + const maxWidth2 = 50; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe(`ζ₯ζ¬γγγ«\nγ‘γ―οΌγγ\nγ―γγΉγγ§\nγγ + θ¦γ¦γΏ\nγΎγγγοΌ\nε\nοΏ₯1234\nγι«γγ + οΌζ¬\nεΌ§οΌγθͺ\nηΉγε₯ηΉγ + η©Ίη½\nζΉθ‘γε
¨θ§\nθ¨ε·β¦γΌ`); + }); + + it("should break Korean sentences", () => { + const text = `νκ΅ μλ
νμΈμ! μ΄κ²μ ν
μ€νΈμ
λλ€. +μ°λ¦¬ 보μ: μνβ©1234γλΉμΈλ€γ +(κ΄νΈ), μΌν, λ§μΉ¨ν. +곡백 μ€λ°κΏγμ κ°κΈ°νΈβ¦β`; + + const maxWidth1 = 80; + const res1 = wrapText(text, font, maxWidth1); + expect(res1).toBe(`νκ΅ μλ
νμΈ\nμ! μ΄κ²μ ν
\nμ€νΈμ
λλ€. +μ°λ¦¬ 보μ: μ\nνβ©1234γλΉ\nμΈλ€γ +(κ΄νΈ), μΌ\nν, λ§μΉ¨ν. +곡백 μ€λ°κΏγμ \nκ°κΈ°νΈβ¦β`); + + const maxWidth2 = 60; + const res2 = wrapText(text, font, maxWidth2); + expect(res2).toBe(`νκ΅ μλ
ν\nμΈμ! μ΄κ²\nμ ν
μ€νΈμ
\nλλ€. +μ°λ¦¬ 보μ:\nμν\nβ©1234\nγλΉμΈλ€γ +(κ΄νΈ),\nμΌν, λ§μΉ¨\nν. +곡백 μ€λ°κΏ\nμ κ°κΈ°νΈβ¦β`); + }); + }); + + describe("When text contains leading whitespaces", () => { + const text = " \t Hello world"; + + it("should preserve leading whitespaces", () => { + const maxWidth = 120; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(" \t Hello\nworld"); + }); + + it("should break and collapse leading whitespaces when line breaks", () => { + const maxWidth = 60; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("\nHello\nworld"); + }); + + it("should break and collapse leading whitespaces whe words break", () => { + const maxWidth = 30; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("\nHel\nlo\nwor\nld"); + }); + }); + + describe("When text contains trailing whitespaces", () => { + it("shouldn't add new lines for trailing spaces", () => { + const text = "Hello whats up "; + const maxWidth = 190; + const res = wrapText(text, font, maxWidth); + expect(res).toBe(text); + }); + + it("should ignore trailing whitespaces when line breaks", () => { + const text = "Hippopotomonstrosesquippedaliophobia ??????"; + const maxWidth = 400; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hippopotomonstrosesquippedaliophobia\n??????"); + }); + + it("should not ignore trailing whitespaces when word breaks", () => { + const text = "Hippopotomonstrosesquippedaliophobia ??????"; + const maxWidth = 300; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hippopotomonstrosesquippedalio\nphobia ??????"); + }); + + it("should ignore trailing whitespaces when word breaks and line breaks", () => { + const text = "Hippopotomonstrosesquippedaliophobia ??????"; + const maxWidth = 180; + const res = wrapText(text, font, maxWidth); + expect(res).toBe("Hippopotomonstrose\nsquippedaliophobia\n??????"); + }); + }); + + describe("When text doesn't contain new lines", () => { + const text = "Hello whats up"; + + [ + { + desc: "break all words when width of each word is less than container width", + width: 70, + res: `Hello\nwhats\nup`, + }, + { + desc: "break all characters when width of each character is less than container width", + width: 15, + res: `H\ne\nl\nl\no\nw\nh\na\nt\ns\nu\np`, + }, + { + desc: "break words as per the width", + + width: 130, + res: `Hello whats\nup`, + }, + { + desc: "fit the container", + + width: 240, + res: "Hello whats up", + }, + { + desc: "push the word if its equal to max width", + width: 50, + res: `Hello\nwhats\nup`, + }, + ].forEach((data) => { + it(`should ${data.desc}`, () => { + const res = wrapText(text, font, data.width); + expect(res).toEqual(data.res); + }); + }); + }); + + describe("When text contain new lines", () => { + const text = `Hello\n whats up`; + [ + { + desc: "break all words when width of each word is less than container width", + width: 70, + res: `Hello\n whats\nup`, + }, + { + desc: "break all characters when width of each character is less than container width", + width: 15, + res: `H\ne\nl\nl\no\n\nw\nh\na\nt\ns\nu\np`, + }, + { + desc: "break words as per the width", + width: 140, + res: `Hello\n whats up`, + }, + ].forEach((data) => { + it(`should respect new lines and ${data.desc}`, () => { + const res = wrapText(text, font, data.width); + expect(res).toEqual(data.res); + }); + }); + }); + + describe("When text is long", () => { + const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`; + [ + { + desc: "fit characters of long string as per container width", + width: 160, + res: `hellolongtextthi\nsiswhatsupwithyo\nuIamtypingggggan\ndtypinggg break\nit now`, + }, + { + desc: "fit characters of long string as per container width and break words as per the width", + + width: 120, + res: `hellolongtex\ntthisiswhats\nupwithyouIam\ntypingggggan\ndtypinggg\nbreak it now`, + }, + { + desc: "fit the long text when container width is greater than text length and move the rest to next line", + + width: 590, + res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg\nbreak it now`, + }, + ].forEach((data) => { + it(`should ${data.desc}`, () => { + const res = wrapText(text, font, data.width); + expect(res).toEqual(data.res); + }); + }); + }); + + describe("Test parseTokens", () => { + it("should tokenize latin", () => { + let text = "Excalidraw is a virtual collaborative whiteboard"; + + expect(parseTokens(text)).toEqual([ + "Excalidraw", + " ", + "is", + " ", + "a", + " ", + "virtual", + " ", + "collaborative", + " ", + "whiteboard", + ]); + + text = + "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects"; + expect(parseTokens(text)).toEqual([ + "Wikipedia", + " ", + "is", + " ", + "hosted", + " ", + "by", + " ", + "Wikimedia-", + " ", + "Foundation,", + " ", + "a", + " ", + "non-", + "profit", + " ", + "organization", + " ", + "that", + " ", + "also", + " ", + "hosts", + " ", + "a", + " ", + "range-", + "of", + " ", + "other", + " ", + "projects", + ]); + }); + + it("should not tokenize number", () => { + const text = "99,100.99"; + const tokens = parseTokens(text); + expect(tokens).toEqual(["99,100.99"]); + }); + + it("should tokenize joined emojis", () => { + const text = `π¬ππΊπ₯βοΈπ©π½βπ¦°π¨βπ©βπ§βπ¦π©πΎβπ¬π³οΈβππ§ββοΈπ§βπ€βπ§π
π½ββοΈβ
0οΈβ£π¨πΏπ¦
`; + const tokens = parseTokens(text); + + expect(tokens).toEqual([ + "π¬", + "π", + "πΊ", + "π₯", + "βοΈ", + "π©π½βπ¦°", + "π¨βπ©βπ§βπ¦", + "π©πΎβπ¬", + "π³οΈβπ", + "π§ββοΈ", + "π§βπ€βπ§", + "π
π½ββοΈ", + "β
", + "0οΈβ£", + "π¨πΏ", + "π¦
", + ]); + }); + + it("should tokenize emojis mixed with mixed text", () => { + const text = `π¬aπbπΊcπ₯dβοΈγπ©π½βπ¦°γπ¨βπ©βπ§βπ¦εΎ·π©πΎβπ¬γπ³οΈβπμπ§ββοΈgπ§βπ€βπ§hπ
π½ββοΈeβ
f0οΈβ£gπ¨πΏ10π¦
#hash`; + const tokens = parseTokens(text); + + expect(tokens).toEqual([ + "π¬", + "a", + "π", + "b", + "πΊ", + "c", + "π₯", + "d", + "βοΈ", + "γ", + "π©π½βπ¦°", + "γ", + "π¨βπ©βπ§βπ¦", + "εΎ·", + "π©πΎβπ¬", + "γ", + "π³οΈβπ", + "μ", + "π§ββοΈ", + "g", + "π§βπ€βπ§", + "h", + "π
π½ββοΈ", + "e", + "β
", + "f0οΈβ£g", // bummer, but ok, as we traded kecaps not breaking (less common) for hash and numbers not breaking (more common) + "π¨πΏ", + "10", // nice! do not break the number, as it's by default matched by \p{Emoji} + "π¦
", + "#hash", // nice! do not break the hash, as it's by default matched by \p{Emoji} + ]); + }); + + it("should tokenize decomposed chars into their composed variants", () => { + // each input character is in a decomposed form + const text = "cΜγ¦γaΜγ²γΞ΅Μαα
‘ΠΈΜαα
‘α«"; + expect(text.normalize("NFC").length).toEqual(8); + expect(text).toEqual(text.normalize("NFD")); + + const tokens = parseTokens(text); + expect(tokens.length).toEqual(8); + expect(tokens).toEqual(["Δ", "γ§", "Γ€", "γ΄", "Ξ", "λ€", "ΠΉ", "ν"]); + }); + + it("should tokenize artificial CJK", () => { + const text = `γιεΎ·ηΆγι«-ι«γγγ«γ‘γ―δΈηοΌμλ
νμΈμμΈκ³οΌμγ,λ€.λ€...μ/λ¬(((λ€)))[[1]]γ({((ν))>)γ(γγγ)γβ¦[Hello] \tγWorldοΌγγ₯γΌγ¨γΌγ―γ»οΏ₯3700.55γγ090-1234-5678οΏ₯1,000γοΌ5,000γη΄ ζ΄γγγοΌγγιθ¦γοΌοΌοΌTaroε30οΌ
γ―γοΌγγͺγ°γοΌγ°οΏ₯110Β±οΏ₯570γ§20βγ9:30γ10:00γδΈηͺγ`; + // [ + // 'γι', 'εΎ·', 'ηΆγ', 'ι«-', + // 'ι«', 'γ', 'γ', 'γ«', + // 'γ‘', 'γ―', 'δΈ', 'ηοΌ', + // 'μ', 'λ
', 'ν', 'μΈ', + // 'μ', 'μΈ', 'κ³οΌ', 'μγ,', + // 'λ€.', 'λ€...', 'μ/', 'λ¬', + // '(((λ€)))', '[[1]]', 'γ({((ν))>)γ', '(γγγ)', + // 'γβ¦', '[Hello]', ' ', '\t', + // 'γ', 'WorldοΌ', 'γ', 'γ₯', + // 'γΌ', 'γ¨', 'γΌ', 'γ―γ»', + // 'οΏ₯3700.55', 'γγ', '090-', '1234-', + // '5678', 'οΏ₯1,000γ', 'οΌ5,000', 'γη΄ ', + // 'ζ΄', 'γ', 'γ', 'γοΌγ', + // 'γι', 'θ¦γ', 'οΌ', 'οΌοΌ', + // 'Taro', 'ε', '30οΌ
', 'γ―γ', + // 'οΌγ', 'γͺ', 'γ°', 'γοΌ', + // 'γ°', 'οΏ₯110Β±', 'οΏ₯570', 'γ§', + // '20βγ', '9:30γ', '10:00', 'γδΈ', + // 'ηͺγ' + // ] + const tokens = parseTokens(text); + + // Latin + expect(tokens).toContain("[[1]]"); + expect(tokens).toContain("[Hello]"); + expect(tokens).toContain("WorldοΌ"); + expect(tokens).toContain("Taro"); + + // Chinese + expect(tokens).toContain("γι"); + expect(tokens).toContain("εΎ·"); + expect(tokens).toContain("ηΆγ"); + expect(tokens).toContain("ι«-"); + expect(tokens).toContain("ι«"); + + // Japanese + expect(tokens).toContain("γ"); + expect(tokens).toContain("γ"); + expect(tokens).toContain("γ«"); + expect(tokens).toContain("γ‘"); + expect(tokens).toContain("γ―"); + expect(tokens).toContain("δΈ"); + expect(tokens).toContain("γ―γ»"); + expect(tokens).toContain("ηοΌ"); + expect(tokens).toContain("γβ¦"); + expect(tokens).toContain("γγ"); + expect(tokens).toContain("γ₯"); + expect(tokens).toContain("γη΄ "); + expect(tokens).toContain("ζ΄"); + expect(tokens).toContain("γ"); + expect(tokens).toContain("γ"); + expect(tokens).toContain("γοΌγ"); + expect(tokens).toContain("ε"); + expect(tokens).toContain("γ―γ"); + expect(tokens).toContain("οΌγ"); + expect(tokens).toContain("γͺ"); + expect(tokens).toContain("γ°"); + expect(tokens).toContain("γοΌ"); + expect(tokens).toContain("γ§"); + expect(tokens).toContain("γδΈ"); + expect(tokens).toContain("ηͺγ"); + + // Check for Korean + expect(tokens).toContain("μ"); + expect(tokens).toContain("λ
"); + expect(tokens).toContain("ν"); + expect(tokens).toContain("μΈ"); + expect(tokens).toContain("μ"); + expect(tokens).toContain("μΈ"); + expect(tokens).toContain("κ³οΌ"); + expect(tokens).toContain("μγ,"); + expect(tokens).toContain("λ€."); + expect(tokens).toContain("λ€..."); + expect(tokens).toContain("μ/"); + expect(tokens).toContain("λ¬"); + expect(tokens).toContain("(((λ€)))"); + expect(tokens).toContain("γ({((ν))>)γ"); + expect(tokens).toContain("(γγγ)"); + + // Numbers and units + expect(tokens).toContain("οΏ₯3700.55"); + expect(tokens).toContain("090-"); + expect(tokens).toContain("1234-"); + expect(tokens).toContain("5678"); + expect(tokens).toContain("οΏ₯1,000γ"); + expect(tokens).toContain("οΌ5,000"); + expect(tokens).toContain("οΌοΌ"); + expect(tokens).toContain("30οΌ
"); + expect(tokens).toContain("οΏ₯110Β±"); + expect(tokens).toContain("20βγ"); + expect(tokens).toContain("9:30γ"); + expect(tokens).toContain("10:00"); + + // Punctuation and symbols + expect(tokens).toContain(" "); + expect(tokens).toContain("\t"); + expect(tokens).toContain("γ"); + expect(tokens).toContain("γ"); + expect(tokens).toContain("γΌ"); + expect(tokens).toContain("γ¨"); + expect(tokens).toContain("γ°"); + expect(tokens).toContain("οΌ"); + }); + }); +}); |
