fractionalIndex.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /* eslint-disable no-lone-blocks */
  2. import { generateKeyBetween } from "fractional-indexing";
  3. import { arrayToMap } from "@excalidraw/common";
  4. import {
  5. syncInvalidIndices,
  6. syncMovedIndices,
  7. validateFractionalIndices,
  8. } from "@excalidraw/element/fractionalIndex";
  9. import { deepCopyElement } from "@excalidraw/element/duplicate";
  10. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  11. import type {
  12. ExcalidrawElement,
  13. FractionalIndex,
  14. } from "@excalidraw/element/types";
  15. import { InvalidFractionalIndexError } from "../src/fractionalIndex";
  16. describe("sync invalid indices with array order", () => {
  17. describe("should NOT sync empty array", () => {
  18. testMovedIndicesSync({
  19. elements: [],
  20. movedElements: [],
  21. expect: {
  22. unchangedElements: [],
  23. validInput: true,
  24. },
  25. });
  26. testInvalidIndicesSync({
  27. elements: [],
  28. expect: {
  29. unchangedElements: [],
  30. validInput: true,
  31. },
  32. });
  33. });
  34. describe("should NOT sync when index is well defined", () => {
  35. testMovedIndicesSync({
  36. elements: [{ id: "A", index: "a1" }],
  37. movedElements: [],
  38. expect: {
  39. unchangedElements: ["A"],
  40. validInput: true,
  41. },
  42. });
  43. testInvalidIndicesSync({
  44. elements: [{ id: "A", index: "a1" }],
  45. expect: {
  46. unchangedElements: ["A"],
  47. validInput: true,
  48. },
  49. });
  50. });
  51. describe("should NOT sync when indices are well defined", () => {
  52. testMovedIndicesSync({
  53. elements: [
  54. { id: "A", index: "a1" },
  55. { id: "B", index: "a2" },
  56. { id: "C", index: "a3" },
  57. ],
  58. movedElements: [],
  59. expect: {
  60. unchangedElements: ["A", "B", "C"],
  61. validInput: true,
  62. },
  63. });
  64. testInvalidIndicesSync({
  65. elements: [
  66. { id: "A", index: "a1" },
  67. { id: "B", index: "a2" },
  68. { id: "C", index: "a3" },
  69. ],
  70. expect: {
  71. unchangedElements: ["A", "B", "C"],
  72. validInput: true,
  73. },
  74. });
  75. });
  76. describe("should sync when fractional index is not defined", () => {
  77. testMovedIndicesSync({
  78. elements: [{ id: "A" }],
  79. movedElements: ["A"],
  80. expect: {
  81. unchangedElements: [],
  82. },
  83. });
  84. testInvalidIndicesSync({
  85. elements: [{ id: "A" }],
  86. expect: {
  87. unchangedElements: [],
  88. },
  89. });
  90. });
  91. describe("should sync when fractional indices are duplicated", () => {
  92. testInvalidIndicesSync({
  93. elements: [
  94. { id: "A", index: "a1" },
  95. { id: "B", index: "a1" },
  96. ],
  97. expect: {
  98. unchangedElements: ["A"],
  99. },
  100. });
  101. testInvalidIndicesSync({
  102. elements: [
  103. { id: "A", index: "a1" },
  104. { id: "B", index: "a1" },
  105. ],
  106. expect: {
  107. unchangedElements: ["A"],
  108. },
  109. });
  110. });
  111. describe("should sync when a fractional index is out of order", () => {
  112. testMovedIndicesSync({
  113. elements: [
  114. { id: "A", index: "a2" },
  115. { id: "B", index: "a1" },
  116. ],
  117. movedElements: ["B"],
  118. expect: {
  119. unchangedElements: ["A"],
  120. },
  121. });
  122. testMovedIndicesSync({
  123. elements: [
  124. { id: "A", index: "a2" },
  125. { id: "B", index: "a1" },
  126. ],
  127. movedElements: ["A"],
  128. expect: {
  129. unchangedElements: ["B"],
  130. },
  131. });
  132. testInvalidIndicesSync({
  133. elements: [
  134. { id: "A", index: "a2" },
  135. { id: "B", index: "a1" },
  136. ],
  137. expect: {
  138. unchangedElements: ["A"],
  139. },
  140. });
  141. });
  142. describe("should sync when fractional indices are out of order", () => {
  143. testMovedIndicesSync({
  144. elements: [
  145. { id: "A", index: "a3" },
  146. { id: "B", index: "a2" },
  147. { id: "C", index: "a1" },
  148. ],
  149. movedElements: ["B", "C"],
  150. expect: {
  151. unchangedElements: ["A"],
  152. },
  153. });
  154. testInvalidIndicesSync({
  155. elements: [
  156. { id: "A", index: "a3" },
  157. { id: "B", index: "a2" },
  158. { id: "C", index: "a1" },
  159. ],
  160. expect: {
  161. unchangedElements: ["A"],
  162. },
  163. });
  164. });
  165. describe("should sync when incorrect fractional index is in between correct ones ", () => {
  166. testMovedIndicesSync({
  167. elements: [
  168. { id: "A", index: "a1" },
  169. { id: "B", index: "a0" },
  170. { id: "C", index: "a2" },
  171. ],
  172. movedElements: ["B"],
  173. expect: {
  174. unchangedElements: ["A", "C"],
  175. },
  176. });
  177. testInvalidIndicesSync({
  178. elements: [
  179. { id: "A", index: "a1" },
  180. { id: "B", index: "a0" },
  181. { id: "C", index: "a2" },
  182. ],
  183. expect: {
  184. unchangedElements: ["A", "C"],
  185. },
  186. });
  187. });
  188. describe("should sync when incorrect fractional index is on top and duplicated below", () => {
  189. testMovedIndicesSync({
  190. elements: [
  191. { id: "A", index: "a1" },
  192. { id: "B", index: "a2" },
  193. { id: "C", index: "a1" },
  194. ],
  195. movedElements: ["C"],
  196. expect: {
  197. unchangedElements: ["A", "B"],
  198. },
  199. });
  200. testInvalidIndicesSync({
  201. elements: [
  202. { id: "A", index: "a1" },
  203. { id: "B", index: "a2" },
  204. { id: "C", index: "a1" },
  205. ],
  206. expect: {
  207. unchangedElements: ["A", "B"],
  208. },
  209. });
  210. });
  211. describe("should sync when given a mix of duplicate / invalid indices", () => {
  212. testMovedIndicesSync({
  213. elements: [
  214. { id: "A", index: "a0" },
  215. { id: "B", index: "a2" },
  216. { id: "C", index: "a1" },
  217. { id: "D", index: "a1" },
  218. { id: "E", index: "a2" },
  219. ],
  220. movedElements: ["C", "D", "E"],
  221. expect: {
  222. unchangedElements: ["A", "B"],
  223. },
  224. });
  225. testInvalidIndicesSync({
  226. elements: [
  227. { id: "A", index: "a0" },
  228. { id: "B", index: "a2" },
  229. { id: "C", index: "a1" },
  230. { id: "D", index: "a1" },
  231. { id: "E", index: "a2" },
  232. ],
  233. expect: {
  234. unchangedElements: ["A", "B"],
  235. },
  236. });
  237. });
  238. describe("should sync when given a mix of undefined / invalid indices", () => {
  239. testMovedIndicesSync({
  240. elements: [
  241. { id: "A" },
  242. { id: "B" },
  243. { id: "C", index: "a0" },
  244. { id: "D", index: "a2" },
  245. { id: "E" },
  246. { id: "F", index: "a3" },
  247. { id: "G" },
  248. { id: "H", index: "a1" },
  249. { id: "I", index: "a2" },
  250. { id: "J" },
  251. ],
  252. movedElements: ["A", "B", "E", "G", "H", "I", "J"],
  253. expect: {
  254. unchangedElements: ["C", "D", "F"],
  255. },
  256. });
  257. testInvalidIndicesSync({
  258. elements: [
  259. { id: "A" },
  260. { id: "B" },
  261. { id: "C", index: "a0" },
  262. { id: "D", index: "a2" },
  263. { id: "E" },
  264. { id: "F", index: "a3" },
  265. { id: "G" },
  266. { id: "H", index: "a1" },
  267. { id: "I", index: "a2" },
  268. { id: "J" },
  269. ],
  270. expect: {
  271. unchangedElements: ["C", "D", "F"],
  272. },
  273. });
  274. });
  275. describe("should sync all moved elements regardless of their validity", () => {
  276. testMovedIndicesSync({
  277. elements: [
  278. { id: "A", index: "a2" },
  279. { id: "B", index: "a4" },
  280. ],
  281. movedElements: ["A"],
  282. expect: {
  283. validInput: true,
  284. unchangedElements: ["B"],
  285. },
  286. });
  287. testMovedIndicesSync({
  288. elements: [
  289. { id: "A", index: "a2" },
  290. { id: "B", index: "a4" },
  291. ],
  292. movedElements: ["B"],
  293. expect: {
  294. validInput: true,
  295. unchangedElements: ["A"],
  296. },
  297. });
  298. testMovedIndicesSync({
  299. elements: [
  300. { id: "C", index: "a2" },
  301. { id: "D", index: "a3" },
  302. { id: "A", index: "a0" },
  303. { id: "B", index: "a1" },
  304. ],
  305. movedElements: ["C", "D"],
  306. expect: {
  307. unchangedElements: ["A", "B"],
  308. },
  309. });
  310. testMovedIndicesSync({
  311. elements: [
  312. { id: "A", index: "a1" },
  313. { id: "B", index: "a2" },
  314. { id: "D", index: "a4" },
  315. { id: "C", index: "a3" },
  316. { id: "F", index: "a6" },
  317. { id: "E", index: "a5" },
  318. { id: "H", index: "a8" },
  319. { id: "G", index: "a7" },
  320. { id: "I", index: "a9" },
  321. ],
  322. movedElements: ["D", "F", "H"],
  323. expect: {
  324. unchangedElements: ["A", "B", "C", "E", "G", "I"],
  325. },
  326. });
  327. {
  328. testMovedIndicesSync({
  329. elements: [
  330. { id: "A", index: "a1" },
  331. { id: "B", index: "a0" },
  332. { id: "C", index: "a2" },
  333. ],
  334. movedElements: ["B", "C"],
  335. expect: {
  336. unchangedElements: ["A"],
  337. },
  338. });
  339. testMovedIndicesSync({
  340. elements: [
  341. { id: "A", index: "a1" },
  342. { id: "B", index: "a0" },
  343. { id: "C", index: "a2" },
  344. ],
  345. movedElements: ["A", "B"],
  346. expect: {
  347. unchangedElements: ["C"],
  348. },
  349. });
  350. }
  351. testMovedIndicesSync({
  352. elements: [
  353. { id: "A", index: "a0" },
  354. { id: "B", index: "a2" },
  355. { id: "C", index: "a1" },
  356. { id: "D", index: "a1" },
  357. { id: "E", index: "a2" },
  358. ],
  359. movedElements: ["B", "D", "E"],
  360. expect: {
  361. unchangedElements: ["A", "C"],
  362. },
  363. });
  364. testMovedIndicesSync({
  365. elements: [
  366. { id: "A" },
  367. { id: "B" },
  368. { id: "C", index: "a0" },
  369. { id: "D", index: "a2" },
  370. { id: "E" },
  371. { id: "F", index: "a3" },
  372. { id: "G" },
  373. { id: "H", index: "a1" },
  374. { id: "I", index: "a2" },
  375. { id: "J" },
  376. ],
  377. movedElements: ["A", "B", "D", "E", "F", "G", "J"],
  378. expect: {
  379. unchangedElements: ["C", "H", "I"],
  380. },
  381. });
  382. });
  383. describe("should generate fractions for explicitly moved elements", () => {
  384. describe("should generate a fraction between 'A' and 'C'", () => {
  385. testMovedIndicesSync({
  386. elements: [
  387. { id: "A", index: "a1" },
  388. // doing actual fractions, without jitter 'a1' becomes 'a1V'
  389. // as V is taken as the charset's middle-right value
  390. { id: "B", index: "a1" },
  391. { id: "C", index: "a2" },
  392. ],
  393. movedElements: ["B"],
  394. expect: {
  395. unchangedElements: ["A", "C"],
  396. },
  397. });
  398. testInvalidIndicesSync({
  399. elements: [
  400. { id: "A", index: "a1" },
  401. { id: "B", index: "a1" },
  402. { id: "C", index: "a2" },
  403. ],
  404. expect: {
  405. // as above, B will become fractional
  406. unchangedElements: ["A", "C"],
  407. },
  408. });
  409. });
  410. describe("should generate fractions given duplicated indices", () => {
  411. testMovedIndicesSync({
  412. elements: [
  413. { id: "A", index: "a01" },
  414. { id: "B", index: "a01" },
  415. { id: "C", index: "a01" },
  416. { id: "D", index: "a01" },
  417. { id: "E", index: "a02" },
  418. { id: "F", index: "a02" },
  419. { id: "G", index: "a02" },
  420. ],
  421. movedElements: ["B", "C", "D", "E", "F"],
  422. expect: {
  423. unchangedElements: ["A", "G"],
  424. },
  425. });
  426. testMovedIndicesSync({
  427. elements: [
  428. { id: "A", index: "a01" },
  429. { id: "B", index: "a01" },
  430. { id: "C", index: "a01" },
  431. { id: "D", index: "a01" },
  432. { id: "E", index: "a02" },
  433. { id: "F", index: "a02" },
  434. { id: "G", index: "a02" },
  435. ],
  436. movedElements: ["A", "C", "D", "E", "G"],
  437. expect: {
  438. unchangedElements: ["B", "F"],
  439. },
  440. });
  441. testMovedIndicesSync({
  442. elements: [
  443. { id: "A", index: "a01" },
  444. { id: "B", index: "a01" },
  445. { id: "C", index: "a01" },
  446. { id: "D", index: "a01" },
  447. { id: "E", index: "a02" },
  448. { id: "F", index: "a02" },
  449. { id: "G", index: "a02" },
  450. ],
  451. movedElements: ["B", "C", "D", "F", "G"],
  452. expect: {
  453. unchangedElements: ["A", "E"],
  454. },
  455. });
  456. testInvalidIndicesSync({
  457. elements: [
  458. { id: "A", index: "a01" },
  459. { id: "B", index: "a01" },
  460. { id: "C", index: "a01" },
  461. { id: "D", index: "a01" },
  462. { id: "E", index: "a02" },
  463. { id: "F", index: "a02" },
  464. { id: "G", index: "a02" },
  465. ],
  466. expect: {
  467. // notice fallback considers first item (E) as a valid one
  468. unchangedElements: ["A", "E"],
  469. },
  470. });
  471. });
  472. });
  473. describe("should be able to sync 20K invalid indices", () => {
  474. const length = 20_000;
  475. describe("should sync all empty indices", () => {
  476. const elements = Array.from({ length }).map((_, index) => ({
  477. id: `A_${index}`,
  478. }));
  479. testMovedIndicesSync({
  480. // elements without fractional index
  481. elements,
  482. movedElements: Array.from({ length }).map((_, index) => `A_${index}`),
  483. expect: {
  484. unchangedElements: [],
  485. },
  486. });
  487. testInvalidIndicesSync({
  488. // elements without fractional index
  489. elements,
  490. expect: {
  491. unchangedElements: [],
  492. },
  493. });
  494. });
  495. describe("should sync all but last index given a growing array of indices", () => {
  496. let lastIndex: string | null = null;
  497. const elements = Array.from({ length }).map((_, index) => {
  498. // going up from 'a0'
  499. lastIndex = generateKeyBetween(lastIndex, null);
  500. return {
  501. id: `A_${index}`,
  502. // assigning the last generated index, so sync can go down from there
  503. // without jitter lastIndex is 'c4BZ' for 20000th element
  504. index: index === length - 1 ? lastIndex : undefined,
  505. };
  506. });
  507. const movedElements = Array.from({ length }).map(
  508. (_, index) => `A_${index}`,
  509. );
  510. // remove last element
  511. movedElements.pop();
  512. testMovedIndicesSync({
  513. elements,
  514. movedElements,
  515. expect: {
  516. unchangedElements: [`A_${length - 1}`],
  517. },
  518. });
  519. testInvalidIndicesSync({
  520. elements,
  521. expect: {
  522. unchangedElements: [`A_${length - 1}`],
  523. },
  524. });
  525. });
  526. describe("should sync all but first index given a declining array of indices", () => {
  527. let lastIndex: string | null = null;
  528. const elements = Array.from({ length }).map((_, index) => {
  529. // going down from 'a0'
  530. lastIndex = generateKeyBetween(null, lastIndex);
  531. return {
  532. id: `A_${index}`,
  533. // without jitter lastIndex is 'XvoR' for 20000th element
  534. index: lastIndex,
  535. };
  536. });
  537. const movedElements = Array.from({ length }).map(
  538. (_, index) => `A_${index}`,
  539. );
  540. // remove first element
  541. movedElements.shift();
  542. testMovedIndicesSync({
  543. elements,
  544. movedElements,
  545. expect: {
  546. unchangedElements: [`A_0`],
  547. },
  548. });
  549. testInvalidIndicesSync({
  550. elements,
  551. expect: {
  552. unchangedElements: [`A_0`],
  553. },
  554. });
  555. });
  556. });
  557. describe("should automatically fallback to fixing all invalid indices", () => {
  558. describe("should fallback to syncing duplicated indices when moved elements are empty", () => {
  559. testMovedIndicesSync({
  560. elements: [
  561. { id: "A", index: "a1" },
  562. { id: "B", index: "a1" },
  563. { id: "C", index: "a1" },
  564. ],
  565. // the validation will throw as nothing was synced
  566. // therefore it will lead to triggering the fallback and fixing all invalid indices
  567. movedElements: [],
  568. expect: {
  569. unchangedElements: ["A"],
  570. },
  571. });
  572. });
  573. describe("should fallback to syncing undefined / invalid indices when moved elements are empty", () => {
  574. testMovedIndicesSync({
  575. elements: [
  576. { id: "A", index: "a1" },
  577. { id: "B" },
  578. { id: "C", index: "a0" },
  579. ],
  580. // since elements are invalid, this will fail the validation
  581. // leading to fallback fixing "B" and "C"
  582. movedElements: [],
  583. expect: {
  584. unchangedElements: ["A"],
  585. },
  586. });
  587. });
  588. describe("should fallback to syncing unordered indices when moved element is invalid", () => {
  589. testMovedIndicesSync({
  590. elements: [
  591. { id: "A", index: "a1" },
  592. { id: "B", index: "a2" },
  593. { id: "C", index: "a1" },
  594. ],
  595. movedElements: ["A"],
  596. expect: {
  597. unchangedElements: ["A", "B"],
  598. },
  599. });
  600. });
  601. describe("should fallback when trying to generate an index in between unordered elements", () => {
  602. testMovedIndicesSync({
  603. elements: [
  604. { id: "A", index: "a2" },
  605. { id: "B" },
  606. { id: "C", index: "a1" },
  607. ],
  608. // 'B' is invalid, but so is 'C', which was not marked as moved
  609. // therefore it will try to generate a key between 'a2' and 'a1'
  610. // which it cannot do, thus will throw during generation and automatically fallback
  611. movedElements: ["B"],
  612. expect: {
  613. unchangedElements: ["A"],
  614. },
  615. });
  616. });
  617. describe("should fallback when trying to generate an index in between duplicate indices", () => {
  618. testMovedIndicesSync({
  619. elements: [
  620. { id: "A", index: "a01" },
  621. { id: "B" },
  622. { id: "C" },
  623. { id: "D", index: "a01" },
  624. { id: "E", index: "a01" },
  625. { id: "F", index: "a01" },
  626. { id: "G" },
  627. { id: "I", index: "a03" },
  628. { id: "H" },
  629. ],
  630. // missed "E" therefore upper bound for 'B' is a01, while lower bound is 'a02'
  631. // therefore, similarly to above, it will fail during key generation and lead to fallback
  632. movedElements: ["B", "C", "D", "F", "G", "H"],
  633. expect: {
  634. unchangedElements: ["A", "I"],
  635. },
  636. });
  637. });
  638. });
  639. });
  640. function testMovedIndicesSync(args: {
  641. elements: { id: string; index?: string }[];
  642. movedElements: string[];
  643. expect: {
  644. unchangedElements: string[];
  645. validInput?: true;
  646. };
  647. }) {
  648. const [elements, movedElements] = prepareArguments(
  649. args.elements,
  650. args.movedElements,
  651. );
  652. const expectUnchangedElements = arrayToMap(
  653. args.expect.unchangedElements.map((x) => ({ id: x })),
  654. );
  655. test(
  656. "should sync invalid indices of moved elements or fallback",
  657. elements,
  658. movedElements,
  659. expectUnchangedElements,
  660. args.expect.validInput,
  661. );
  662. }
  663. function testInvalidIndicesSync(args: {
  664. elements: { id: string; index?: string }[];
  665. expect: {
  666. unchangedElements: string[];
  667. validInput?: true;
  668. };
  669. }) {
  670. const [elements] = prepareArguments(args.elements);
  671. const expectUnchangedElements = arrayToMap(
  672. args.expect.unchangedElements.map((x) => ({ id: x })),
  673. );
  674. test(
  675. "should sync invalid indices of all elements",
  676. elements,
  677. undefined,
  678. expectUnchangedElements,
  679. args.expect.validInput,
  680. );
  681. }
  682. function prepareArguments(
  683. elementsLike: { id: string; index?: string }[],
  684. movedElementsIds?: string[],
  685. ): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
  686. const elements = elementsLike.map((x) =>
  687. API.createElement({ id: x.id, index: x.index as FractionalIndex }),
  688. );
  689. const movedMap = arrayToMap(movedElementsIds || []);
  690. const movedElements = movedElementsIds
  691. ? arrayToMap(elements.filter((x) => movedMap.has(x.id)))
  692. : undefined;
  693. return [elements, movedElements];
  694. }
  695. function test(
  696. name: string,
  697. elements: ExcalidrawElement[],
  698. movedElements: Map<string, ExcalidrawElement> | undefined,
  699. expectUnchangedElements: Map<string, { id: string }>,
  700. expectValidInput?: boolean,
  701. ) {
  702. it(name, () => {
  703. // ensure the input is invalid (unless the flag is on)
  704. if (!expectValidInput) {
  705. expect(() =>
  706. validateFractionalIndices(elements, {
  707. shouldThrow: true,
  708. includeBoundTextValidation: true,
  709. ignoreLogs: true,
  710. }),
  711. ).toThrowError(InvalidFractionalIndexError);
  712. }
  713. // clone due to mutation
  714. const clonedElements = elements.map((x) => deepCopyElement(x));
  715. // act
  716. const syncedElements = movedElements
  717. ? syncMovedIndices(clonedElements, movedElements)
  718. : syncInvalidIndices(clonedElements);
  719. expect(syncedElements.length).toBe(elements.length);
  720. expect(() =>
  721. validateFractionalIndices(syncedElements, {
  722. shouldThrow: true,
  723. includeBoundTextValidation: true,
  724. ignoreLogs: true,
  725. }),
  726. ).not.toThrowError(InvalidFractionalIndexError);
  727. syncedElements.forEach((synced, index) => {
  728. const element = elements[index];
  729. // ensure the order hasn't changed
  730. expect(synced.id).toBe(element.id);
  731. if (expectUnchangedElements.has(synced.id)) {
  732. // ensure we didn't mutate where we didn't want to mutate
  733. expect(synced.index).toBe(elements[index].index);
  734. expect(synced.version).toBe(elements[index].version);
  735. } else {
  736. expect(synced.index).not.toBe(elements[index].index);
  737. // ensure we mutated just once, even with fallback triggered
  738. expect(synced.version).toBe(elements[index].version + 1);
  739. }
  740. });
  741. });
  742. }