fractionalIndex.test.ts 21 KB

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