parser.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { describe, it, expect } from "vitest";
  2. import { createYamlStreamCompiler } from "./parser";
  3. describe("createYamlStreamCompiler", () => {
  4. it("parses a simple YAML document incrementally", () => {
  5. const compiler = createYamlStreamCompiler();
  6. const r1 = compiler.push("root: main\n");
  7. expect(r1.newPatches.length).toBeGreaterThan(0);
  8. expect(r1.result).toHaveProperty("root", "main");
  9. });
  10. it("accumulates elements as lines arrive", () => {
  11. const compiler = createYamlStreamCompiler();
  12. compiler.push("root: main\n");
  13. compiler.push("elements:\n");
  14. compiler.push(" main:\n");
  15. compiler.push(" type: Card\n");
  16. const { result } = compiler.flush();
  17. expect(result).toHaveProperty("root", "main");
  18. const elements = result.elements as Record<string, Record<string, unknown>>;
  19. expect(elements.main).toBeDefined();
  20. expect(elements.main!.type).toBe("Card");
  21. });
  22. it("emits patches only for changes", () => {
  23. const compiler = createYamlStreamCompiler();
  24. const r1 = compiler.push("root: main\n");
  25. expect(r1.newPatches).toEqual([
  26. { op: "add", path: "/root", value: "main" },
  27. ]);
  28. // Pushing the same content again (no new complete line) should not emit patches
  29. const r2 = compiler.push("");
  30. expect(r2.newPatches).toEqual([]);
  31. });
  32. it("tracks all patches via getPatches()", () => {
  33. const compiler = createYamlStreamCompiler();
  34. compiler.push("a: 1\n");
  35. compiler.push("b: 2\n");
  36. const allPatches = compiler.getPatches();
  37. expect(allPatches.length).toBe(2);
  38. expect(allPatches[0]).toEqual({ op: "add", path: "/a", value: 1 });
  39. expect(allPatches[1]).toEqual({ op: "add", path: "/b", value: 2 });
  40. });
  41. it("resets to initial state", () => {
  42. const compiler = createYamlStreamCompiler();
  43. compiler.push("root: main\n");
  44. expect(compiler.getResult()).toHaveProperty("root", "main");
  45. compiler.reset();
  46. expect(compiler.getResult()).toEqual({});
  47. expect(compiler.getPatches()).toEqual([]);
  48. });
  49. it("resets with initial value and diffs from it", () => {
  50. const compiler = createYamlStreamCompiler();
  51. compiler.reset({ root: "existing", elements: {} });
  52. // The YAML includes root, so the initial value is preserved in the diff base
  53. const { newPatches } = compiler.push(
  54. "root: existing\nelements:\n main:\n type: Card\n",
  55. );
  56. const { result } = compiler.flush();
  57. expect(result).toHaveProperty("root", "existing");
  58. expect(result).toHaveProperty("elements");
  59. // Only the new element should be patched, not root (unchanged)
  60. expect(newPatches.find((p) => p.path === "/root")).toBeUndefined();
  61. expect(newPatches.find((p) => p.path === "/elements/main")).toBeDefined();
  62. });
  63. it("handles a full spec YAML", () => {
  64. const compiler = createYamlStreamCompiler();
  65. const yaml = [
  66. "root: main\n",
  67. "elements:\n",
  68. " main:\n",
  69. " type: Card\n",
  70. " props:\n",
  71. " title: Dashboard\n",
  72. " children:\n",
  73. " - metric-1\n",
  74. " metric-1:\n",
  75. " type: Metric\n",
  76. " props:\n",
  77. " label: Revenue\n",
  78. ' value: "$1.2M"\n',
  79. " children: []\n",
  80. "state:\n",
  81. " revenue: 1200000\n",
  82. ];
  83. for (const line of yaml) {
  84. compiler.push(line);
  85. }
  86. const { result } = compiler.flush();
  87. expect(result.root).toBe("main");
  88. expect(result.state).toEqual({ revenue: 1200000 });
  89. const elements = result.elements as Record<string, Record<string, unknown>>;
  90. expect(elements.main).toBeDefined();
  91. expect(elements["metric-1"]).toBeDefined();
  92. expect((elements["metric-1"]!.props as Record<string, unknown>).label).toBe(
  93. "Revenue",
  94. );
  95. });
  96. it("does not crash on invalid YAML mid-stream", () => {
  97. const compiler = createYamlStreamCompiler();
  98. // Partial YAML that won't parse
  99. compiler.push("elements:\n");
  100. compiler.push(" main:\n");
  101. compiler.push(" type: "); // incomplete value — no newline yet
  102. // Should not throw, result should still be from last successful parse
  103. const r = compiler.push("\n");
  104. expect(r.result).toBeDefined();
  105. });
  106. it("YAML 1.2 does not coerce yes/no/on/off to booleans", () => {
  107. const compiler = createYamlStreamCompiler();
  108. compiler.push("active: yes\n");
  109. compiler.push("disabled: no\n");
  110. compiler.push("on_value: on\n");
  111. compiler.push("off_value: off\n");
  112. const { result } = compiler.flush();
  113. // YAML 1.2 (yaml v2 default) treats these as strings, not booleans
  114. expect(result.active).toBe("yes");
  115. expect(result.disabled).toBe("no");
  116. expect(result.on_value).toBe("on");
  117. expect(result.off_value).toBe("off");
  118. });
  119. });