fractionalIndex.test.ts 21 KB

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