Initial Commit
This commit is contained in:
107
packages/foundation/data/templates/tests/property.test.ts
Normal file
107
packages/foundation/data/templates/tests/property.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Property-based tests using fast-check.
|
||||
*
|
||||
* Covers:
|
||||
* - Idempotent rendering: re-parsing an already-rendered literal produces same output.
|
||||
* - Counter monotone: named counters increase within a single parse call.
|
||||
* - getNestedValue / setNestedValue round-trip.
|
||||
* - PluginPriority strict ordering.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as fc from "fast-check";
|
||||
import { TemplateEngine } from "../src/engine/TemplateEngine.js";
|
||||
import { createCounterPlugin } from "../src/plugins/dynamic/CounterPlugin.js";
|
||||
import { createDotNotationPlugin } from "../src/plugins/variables/DotNotationPlugin.js";
|
||||
import { PluginPriority } from "../src/types/index.js";
|
||||
import { getNestedValue, setNestedValue } from "../src/utils/nested-value.js";
|
||||
|
||||
describe("idempotent literal rendering", () => {
|
||||
it("re-parsing a rendered literal string returns the same string", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
const e = new TemplateEngine().use(createDotNotationPlugin());
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
// Strings with no { or } — guaranteed literals after render
|
||||
fc.string({
|
||||
unit: fc.char().filter((c) => c !== "{" && c !== "}"),
|
||||
}),
|
||||
(literal) => {
|
||||
const first = e.parse(literal, {});
|
||||
const second = e.parse(first, {});
|
||||
expect(second).toBe(first);
|
||||
}
|
||||
),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("counter monotone within single parse", () => {
|
||||
it("counter values strictly increase left-to-right in one parse call", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(fc.integer({ min: 2, max: 10 }), (count) => {
|
||||
const e = new TemplateEngine().use(createCounterPlugin());
|
||||
// Build a template with `count` counter tokens
|
||||
const template = Array.from(
|
||||
{ length: count },
|
||||
() => "{_counter}"
|
||||
).join(" ");
|
||||
const result = e.parse(template, {});
|
||||
const values = result
|
||||
.split(" ")
|
||||
.map((v) => Number.parseInt(v, 10));
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
expect(values[i]).toBeGreaterThan(values[i - 1] as number);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNestedValue / setNestedValue round-trip", () => {
|
||||
it("reading back after set returns the same primitive", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
// Safe path: only lowercase letters and digits, no array notation
|
||||
const safePath = fc
|
||||
.array(fc.stringMatching(/^[a-z][a-z0-9]{0,5}$/), {
|
||||
minLength: 1,
|
||||
maxLength: 4,
|
||||
})
|
||||
.map((parts) => parts.join("."));
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
safePath,
|
||||
fc.oneof(
|
||||
fc.integer(),
|
||||
fc.string(),
|
||||
fc.boolean()
|
||||
// Note: null excluded because getNestedValue returns undefined for null values
|
||||
),
|
||||
(path, value) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
setNestedValue(obj, path, value);
|
||||
const retrieved = getNestedValue(obj, path);
|
||||
expect(retrieved).toBe(value);
|
||||
}
|
||||
),
|
||||
{ numRuns: 300 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginPriority strict ordering", () => {
|
||||
it("MINIMAL < LOW < MEDIUM < HIGH < CRITICAL", { timeout: 5000 }, () => {
|
||||
expect(PluginPriority.MINIMAL).toBeLessThan(PluginPriority.LOW);
|
||||
expect(PluginPriority.LOW).toBeLessThan(PluginPriority.MEDIUM);
|
||||
expect(PluginPriority.MEDIUM).toBeLessThan(PluginPriority.HIGH);
|
||||
expect(PluginPriority.HIGH).toBeLessThan(PluginPriority.CRITICAL);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user