textElement.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import { getLineHeight } from "@excalidraw/common";
  2. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  3. import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
  4. import {
  5. computeContainerDimensionForBoundText,
  6. getContainerCoords,
  7. getBoundTextMaxWidth,
  8. getBoundTextMaxHeight,
  9. computeBoundTextPosition,
  10. } from "../src/textElement";
  11. import { detectLineHeight, getLineHeightInPx } from "../src/textMeasurements";
  12. import type { ExcalidrawTextElementWithContainer } from "../src/types";
  13. describe("Test measureText", () => {
  14. describe("Test getContainerCoords", () => {
  15. const params = { width: 200, height: 100, x: 10, y: 20 };
  16. it("should compute coords correctly when ellipse", () => {
  17. const element = API.createElement({
  18. type: "ellipse",
  19. ...params,
  20. });
  21. expect(getContainerCoords(element)).toEqual({
  22. x: 44.2893218813452455,
  23. y: 39.64466094067262,
  24. });
  25. });
  26. it("should compute coords correctly when rectangle", () => {
  27. const element = API.createElement({
  28. type: "rectangle",
  29. ...params,
  30. });
  31. expect(getContainerCoords(element)).toEqual({
  32. x: 15,
  33. y: 25,
  34. });
  35. });
  36. it("should compute coords correctly when diamond", () => {
  37. const element = API.createElement({
  38. type: "diamond",
  39. ...params,
  40. });
  41. expect(getContainerCoords(element)).toEqual({
  42. x: 65,
  43. y: 50,
  44. });
  45. });
  46. });
  47. describe("Test computeContainerDimensionForBoundText", () => {
  48. const params = {
  49. width: 178,
  50. height: 194,
  51. };
  52. it("should compute container height correctly for rectangle", () => {
  53. const element = API.createElement({
  54. type: "rectangle",
  55. ...params,
  56. });
  57. expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
  58. 160,
  59. );
  60. });
  61. it("should compute container height correctly for ellipse", () => {
  62. const element = API.createElement({
  63. type: "ellipse",
  64. ...params,
  65. });
  66. expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
  67. 226,
  68. );
  69. });
  70. it("should compute container height correctly for diamond", () => {
  71. const element = API.createElement({
  72. type: "diamond",
  73. ...params,
  74. });
  75. expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
  76. 320,
  77. );
  78. });
  79. });
  80. describe("Test getBoundTextMaxWidth", () => {
  81. const params = {
  82. width: 178,
  83. height: 194,
  84. };
  85. it("should return max width when container is rectangle", () => {
  86. const container = API.createElement({ type: "rectangle", ...params });
  87. expect(getBoundTextMaxWidth(container, null)).toBe(168);
  88. });
  89. it("should return max width when container is ellipse", () => {
  90. const container = API.createElement({ type: "ellipse", ...params });
  91. expect(getBoundTextMaxWidth(container, null)).toBe(116);
  92. });
  93. it("should return max width when container is diamond", () => {
  94. const container = API.createElement({ type: "diamond", ...params });
  95. expect(getBoundTextMaxWidth(container, null)).toBe(79);
  96. });
  97. });
  98. describe("Test getBoundTextMaxHeight", () => {
  99. const params = {
  100. width: 178,
  101. height: 194,
  102. id: '"container-id',
  103. };
  104. const boundTextElement = API.createElement({
  105. type: "text",
  106. id: "text-id",
  107. x: 560.51171875,
  108. y: 202.033203125,
  109. width: 154,
  110. height: 175,
  111. fontSize: 20,
  112. fontFamily: 1,
  113. text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
  114. textAlign: "center",
  115. verticalAlign: "middle",
  116. containerId: params.id,
  117. }) as ExcalidrawTextElementWithContainer;
  118. it("should return max height when container is rectangle", () => {
  119. const container = API.createElement({ type: "rectangle", ...params });
  120. expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
  121. });
  122. it("should return max height when container is ellipse", () => {
  123. const container = API.createElement({ type: "ellipse", ...params });
  124. expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
  125. });
  126. it("should return max height when container is diamond", () => {
  127. const container = API.createElement({ type: "diamond", ...params });
  128. expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
  129. });
  130. it("should return max height when container is arrow", () => {
  131. const container = API.createElement({
  132. type: "arrow",
  133. ...params,
  134. });
  135. expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
  136. });
  137. it("should return max height when container is arrow and height is less than threshold", () => {
  138. const container = API.createElement({
  139. type: "arrow",
  140. ...params,
  141. height: 70,
  142. boundElements: [{ type: "text", id: "text-id" }],
  143. });
  144. expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
  145. boundTextElement.height,
  146. );
  147. });
  148. });
  149. });
  150. const textElement = API.createElement({
  151. type: "text",
  152. text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
  153. fontSize: 20,
  154. fontFamily: 1,
  155. height: 175,
  156. });
  157. describe("Test detectLineHeight", () => {
  158. it("should return correct line height", () => {
  159. expect(detectLineHeight(textElement)).toBe(1.25);
  160. });
  161. });
  162. describe("Test getLineHeightInPx", () => {
  163. it("should return correct line height", () => {
  164. expect(
  165. getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
  166. ).toBe(25);
  167. });
  168. });
  169. describe("Test getDefaultLineHeight", () => {
  170. it("should return line height using default font family when not passed", () => {
  171. //@ts-ignore
  172. expect(getLineHeight()).toBe(1.25);
  173. });
  174. it("should return line height using default font family for unknown font", () => {
  175. const UNKNOWN_FONT = 5;
  176. expect(getLineHeight(UNKNOWN_FONT)).toBe(1.25);
  177. });
  178. it("should return correct line height", () => {
  179. expect(getLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
  180. });
  181. });
  182. describe("Test computeBoundTextPosition", () => {
  183. const createMockElementsMap = () => new Map();
  184. // Helper function to create rectangle test case with 90-degree rotation
  185. const createRotatedRectangleTestCase = (
  186. textAlign: string,
  187. verticalAlign: string,
  188. ) => {
  189. const container = API.createElement({
  190. type: "rectangle",
  191. x: 100,
  192. y: 100,
  193. width: 200,
  194. height: 100,
  195. angle: (Math.PI / 2) as any, // 90 degrees
  196. });
  197. const boundTextElement = API.createElement({
  198. type: "text",
  199. width: 80,
  200. height: 40,
  201. text: "hello darkness my old friend",
  202. textAlign: textAlign as any,
  203. verticalAlign: verticalAlign as any,
  204. containerId: container.id,
  205. }) as ExcalidrawTextElementWithContainer;
  206. const elementsMap = createMockElementsMap();
  207. return { container, boundTextElement, elementsMap };
  208. };
  209. describe("90-degree rotation with all alignment combinations", () => {
  210. // Test all 9 combinations of horizontal (left, center, right) and vertical (top, middle, bottom) alignment
  211. it("should position text with LEFT + TOP alignment at 90-degree rotation", () => {
  212. const { container, boundTextElement, elementsMap } =
  213. createRotatedRectangleTestCase(TEXT_ALIGN.LEFT, VERTICAL_ALIGN.TOP);
  214. const result = computeBoundTextPosition(
  215. container,
  216. boundTextElement,
  217. elementsMap,
  218. );
  219. expect(result.x).toBeCloseTo(185, 1);
  220. expect(result.y).toBeCloseTo(75, 1);
  221. });
  222. it("should position text with LEFT + MIDDLE alignment at 90-degree rotation", () => {
  223. const { container, boundTextElement, elementsMap } =
  224. createRotatedRectangleTestCase(TEXT_ALIGN.LEFT, VERTICAL_ALIGN.MIDDLE);
  225. const result = computeBoundTextPosition(
  226. container,
  227. boundTextElement,
  228. elementsMap,
  229. );
  230. expect(result.x).toBeCloseTo(160, 1);
  231. expect(result.y).toBeCloseTo(75, 1);
  232. });
  233. it("should position text with LEFT + BOTTOM alignment at 90-degree rotation", () => {
  234. const { container, boundTextElement, elementsMap } =
  235. createRotatedRectangleTestCase(TEXT_ALIGN.LEFT, VERTICAL_ALIGN.BOTTOM);
  236. const result = computeBoundTextPosition(
  237. container,
  238. boundTextElement,
  239. elementsMap,
  240. );
  241. expect(result.x).toBeCloseTo(135, 1);
  242. expect(result.y).toBeCloseTo(75, 1);
  243. });
  244. it("should position text with CENTER + TOP alignment at 90-degree rotation", () => {
  245. const { container, boundTextElement, elementsMap } =
  246. createRotatedRectangleTestCase(TEXT_ALIGN.CENTER, VERTICAL_ALIGN.TOP);
  247. const result = computeBoundTextPosition(
  248. container,
  249. boundTextElement,
  250. elementsMap,
  251. );
  252. expect(result.x).toBeCloseTo(185, 1);
  253. expect(result.y).toBeCloseTo(130, 1);
  254. });
  255. it("should position text with CENTER + MIDDLE alignment at 90-degree rotation", () => {
  256. const { container, boundTextElement, elementsMap } =
  257. createRotatedRectangleTestCase(
  258. TEXT_ALIGN.CENTER,
  259. VERTICAL_ALIGN.MIDDLE,
  260. );
  261. const result = computeBoundTextPosition(
  262. container,
  263. boundTextElement,
  264. elementsMap,
  265. );
  266. expect(result.x).toBeCloseTo(160, 1);
  267. expect(result.y).toBeCloseTo(130, 1);
  268. });
  269. it("should position text with CENTER + BOTTOM alignment at 90-degree rotation", () => {
  270. const { container, boundTextElement, elementsMap } =
  271. createRotatedRectangleTestCase(
  272. TEXT_ALIGN.CENTER,
  273. VERTICAL_ALIGN.BOTTOM,
  274. );
  275. const result = computeBoundTextPosition(
  276. container,
  277. boundTextElement,
  278. elementsMap,
  279. );
  280. expect(result.x).toBeCloseTo(135, 1);
  281. expect(result.y).toBeCloseTo(130, 1);
  282. });
  283. it("should position text with RIGHT + TOP alignment at 90-degree rotation", () => {
  284. const { container, boundTextElement, elementsMap } =
  285. createRotatedRectangleTestCase(TEXT_ALIGN.RIGHT, VERTICAL_ALIGN.TOP);
  286. const result = computeBoundTextPosition(
  287. container,
  288. boundTextElement,
  289. elementsMap,
  290. );
  291. expect(result.x).toBeCloseTo(185, 1);
  292. expect(result.y).toBeCloseTo(185, 1);
  293. });
  294. it("should position text with RIGHT + MIDDLE alignment at 90-degree rotation", () => {
  295. const { container, boundTextElement, elementsMap } =
  296. createRotatedRectangleTestCase(TEXT_ALIGN.RIGHT, VERTICAL_ALIGN.MIDDLE);
  297. const result = computeBoundTextPosition(
  298. container,
  299. boundTextElement,
  300. elementsMap,
  301. );
  302. expect(result.x).toBeCloseTo(160, 1);
  303. expect(result.y).toBeCloseTo(185, 1);
  304. });
  305. it("should position text with RIGHT + BOTTOM alignment at 90-degree rotation", () => {
  306. const { container, boundTextElement, elementsMap } =
  307. createRotatedRectangleTestCase(TEXT_ALIGN.RIGHT, VERTICAL_ALIGN.BOTTOM);
  308. const result = computeBoundTextPosition(
  309. container,
  310. boundTextElement,
  311. elementsMap,
  312. );
  313. expect(result.x).toBeCloseTo(135, 1);
  314. expect(result.y).toBeCloseTo(185, 1);
  315. });
  316. });
  317. });