|
| 1 | +import { describe, it, expect } from "vitest"; |
| 2 | +import { |
| 3 | + hasTemplateMarkers, |
| 4 | + extractTemplateRefs, |
| 5 | + resolveTemplateMarkers, |
| 6 | + embedsToMarkers, |
| 7 | + markersToEmbeds, |
| 8 | + pickEmbedChar, |
| 9 | +} from "./fnTemplate"; |
| 10 | +import type { Variable } from "../types/Variable"; |
| 11 | + |
| 12 | +const vars: Variable[] = [ |
| 13 | + { id: "a", name: "sku", fnNumber: 1, defaultValue: "DEFAULT-1" }, |
| 14 | + { id: "b", name: "price", fnNumber: 2, defaultValue: "9.99" }, |
| 15 | + { id: "c", name: "lot", fnNumber: 5, defaultValue: "X" }, |
| 16 | +]; |
| 17 | + |
| 18 | +describe("hasTemplateMarkers", () => { |
| 19 | + it("matches single marker", () => { |
| 20 | + expect(hasTemplateMarkers("hello «sku»")).toBe(true); |
| 21 | + }); |
| 22 | + it("matches multiple markers", () => { |
| 23 | + expect(hasTemplateMarkers("«a» and «b»")).toBe(true); |
| 24 | + }); |
| 25 | + it("returns false on plain text", () => { |
| 26 | + expect(hasTemplateMarkers("plain text")).toBe(false); |
| 27 | + }); |
| 28 | + it("does not match an opening guillemet alone", () => { |
| 29 | + expect(hasTemplateMarkers("« no closer")).toBe(false); |
| 30 | + }); |
| 31 | +}); |
| 32 | + |
| 33 | +describe("extractTemplateRefs", () => { |
| 34 | + it("preserves source order and duplicates", () => { |
| 35 | + expect(extractTemplateRefs("«a» and «b» again «a»")).toEqual(["a", "b", "a"]); |
| 36 | + }); |
| 37 | +}); |
| 38 | + |
| 39 | +describe("resolveTemplateMarkers", () => { |
| 40 | + it("substitutes each marker via the resolver", () => { |
| 41 | + const r = resolveTemplateMarkers("«sku»-«price»", (n) => |
| 42 | + n === "sku" ? "ABC" : n === "price" ? "19.99" : undefined, |
| 43 | + ); |
| 44 | + expect(r).toBe("ABC-19.99"); |
| 45 | + }); |
| 46 | + it("leaves unknown markers literal", () => { |
| 47 | + const r = resolveTemplateMarkers("«known»/«missing»", (n) => (n === "known" ? "v" : undefined)); |
| 48 | + expect(r).toBe("v/«missing»"); |
| 49 | + }); |
| 50 | +}); |
| 51 | + |
| 52 | +describe("embedsToMarkers", () => { |
| 53 | + const fnToName = new Map([ |
| 54 | + [1, "sku"], |
| 55 | + [2, "price"], |
| 56 | + ]); |
| 57 | + it("converts whole-field embeds with default # delimiter", () => { |
| 58 | + expect(embedsToMarkers("Hello #1#-#2#", "#", fnToName)).toBe("Hello «sku»-«price»"); |
| 59 | + }); |
| 60 | + it("converts substring embeds (slice args discarded)", () => { |
| 61 | + expect(embedsToMarkers("#1,0,3#", "#", fnToName)).toBe("«sku»"); |
| 62 | + }); |
| 63 | + it("respects a custom embedChar set via ^FE", () => { |
| 64 | + expect(embedsToMarkers("Hello @1@-@2@", "@", fnToName)).toBe("Hello «sku»-«price»"); |
| 65 | + }); |
| 66 | + it("leaves embeds for unknown FN numbers literal (loss-less round-trip)", () => { |
| 67 | + expect(embedsToMarkers("#9#", "#", fnToName)).toBe("#9#"); |
| 68 | + }); |
| 69 | +}); |
| 70 | + |
| 71 | +describe("markersToEmbeds", () => { |
| 72 | + it("emits embeds for known variable names + reports the fnNumbers used", () => { |
| 73 | + const r = markersToEmbeds("«sku»-«price»", vars, "#"); |
| 74 | + expect(r.payload).toBe("#1#-#2#"); |
| 75 | + expect([...r.referencedFnNumbers].sort()).toEqual([1, 2]); |
| 76 | + }); |
| 77 | + it("leaves markers literal when the named variable does not exist", () => { |
| 78 | + const r = markersToEmbeds("«sku»-«gone»", vars, "#"); |
| 79 | + expect(r.payload).toBe("#1#-«gone»"); |
| 80 | + expect([...r.referencedFnNumbers]).toEqual([1]); |
| 81 | + }); |
| 82 | + it("dedupes referencedFnNumbers when the same name appears twice", () => { |
| 83 | + const r = markersToEmbeds("«sku»/«sku»", vars, "#"); |
| 84 | + expect(r.payload).toBe("#1#/#1#"); |
| 85 | + expect([...r.referencedFnNumbers]).toEqual([1]); |
| 86 | + }); |
| 87 | + it("uses a non-default embedChar passed by the caller", () => { |
| 88 | + const r = markersToEmbeds("«sku»-«price»", vars, "@"); |
| 89 | + expect(r.payload).toBe("@1@-@2@"); |
| 90 | + }); |
| 91 | +}); |
| 92 | + |
| 93 | +describe("pickEmbedChar", () => { |
| 94 | + it("returns # when no payload contains it", () => { |
| 95 | + expect(pickEmbedChar(["plain text", "more"])).toBe("#"); |
| 96 | + }); |
| 97 | + it("falls back to next candidate when # appears in any payload", () => { |
| 98 | + expect(pickEmbedChar(["Item #SKU-1", "other"])).toBe("@"); |
| 99 | + }); |
| 100 | + it("returns null when every candidate is taken", () => { |
| 101 | + expect(pickEmbedChar(["#@|%&?!"])).toBe(null); |
| 102 | + }); |
| 103 | +}); |
0 commit comments