diff.test.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { describe, it, expect } from "vitest";
  2. import { diffToPatches } from "./diff";
  3. describe("diffToPatches", () => {
  4. it("returns empty array for identical objects", () => {
  5. const obj = { a: 1, b: "hello" };
  6. expect(diffToPatches(obj, obj)).toEqual([]);
  7. });
  8. it("detects added keys", () => {
  9. const patches = diffToPatches({}, { name: "Alice" });
  10. expect(patches).toEqual([{ op: "add", path: "/name", value: "Alice" }]);
  11. });
  12. it("detects removed keys", () => {
  13. const patches = diffToPatches({ name: "Alice" }, {});
  14. expect(patches).toEqual([{ op: "remove", path: "/name" }]);
  15. });
  16. it("detects changed scalar values", () => {
  17. const patches = diffToPatches({ name: "Alice" }, { name: "Bob" });
  18. expect(patches).toEqual([{ op: "replace", path: "/name", value: "Bob" }]);
  19. });
  20. it("recurses into nested objects", () => {
  21. const oldObj = { user: { name: "Alice", age: 30 } };
  22. const newObj = { user: { name: "Alice", age: 31 } };
  23. const patches = diffToPatches(oldObj, newObj);
  24. expect(patches).toEqual([{ op: "replace", path: "/user/age", value: 31 }]);
  25. });
  26. it("replaces arrays atomically", () => {
  27. const oldObj = { items: ["a", "b"] };
  28. const newObj = { items: ["a", "b", "c"] };
  29. const patches = diffToPatches(oldObj, newObj);
  30. expect(patches).toEqual([
  31. { op: "replace", path: "/items", value: ["a", "b", "c"] },
  32. ]);
  33. });
  34. it("does not emit patch for identical arrays", () => {
  35. const oldObj = { items: [1, 2, 3] };
  36. const newObj = { items: [1, 2, 3] };
  37. expect(diffToPatches(oldObj, newObj)).toEqual([]);
  38. });
  39. it("handles type changes (object → scalar)", () => {
  40. const oldObj = { data: { nested: true } };
  41. const newObj = { data: "flat" };
  42. const patches = diffToPatches(
  43. oldObj as Record<string, unknown>,
  44. newObj as Record<string, unknown>,
  45. );
  46. expect(patches).toEqual([{ op: "replace", path: "/data", value: "flat" }]);
  47. });
  48. it("handles a complex spec diff", () => {
  49. const oldSpec = {
  50. root: "main",
  51. elements: {
  52. main: { type: "Card", props: { title: "Hello" }, children: [] },
  53. },
  54. };
  55. const newSpec = {
  56. root: "main",
  57. elements: {
  58. main: {
  59. type: "Card",
  60. props: { title: "Hello" },
  61. children: ["child-1"],
  62. },
  63. "child-1": {
  64. type: "Text",
  65. props: { content: "World" },
  66. children: [],
  67. },
  68. },
  69. };
  70. const patches = diffToPatches(oldSpec, newSpec);
  71. expect(patches).toContainEqual({
  72. op: "replace",
  73. path: "/elements/main/children",
  74. value: ["child-1"],
  75. });
  76. expect(patches).toContainEqual({
  77. op: "add",
  78. path: "/elements/child-1",
  79. value: {
  80. type: "Text",
  81. props: { content: "World" },
  82. children: [],
  83. },
  84. });
  85. });
  86. it("escapes JSON Pointer tokens (~ and /)", () => {
  87. const patches = diffToPatches({}, { "a/b": 1, "c~d": 2 });
  88. expect(patches).toContainEqual({
  89. op: "add",
  90. path: "/a~1b",
  91. value: 1,
  92. });
  93. expect(patches).toContainEqual({
  94. op: "add",
  95. path: "/c~0d",
  96. value: 2,
  97. });
  98. });
  99. });