| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import { describe, it, expect } from "vitest";
- import type { Spec } from "./types";
- import { validateSpec, autoFixSpec } from "./spec-validator";
- // =============================================================================
- // validateSpec
- // =============================================================================
- describe("validateSpec", () => {
- it("returns valid for a correct spec", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: { type: "Stack", props: {}, children: ["child1"] },
- child1: { type: "Text", props: { text: "hello" }, children: [] },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(true);
- expect(result.issues).toHaveLength(0);
- });
- it("detects missing root", () => {
- const spec = {
- root: "",
- elements: { a: { type: "T", props: {}, children: [] } },
- } as Spec;
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "missing_root")).toBe(true);
- });
- it("detects root_not_found", () => {
- const spec: Spec = {
- root: "missing",
- elements: { a: { type: "T", props: {}, children: [] } },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "root_not_found")).toBe(true);
- });
- it("detects empty spec", () => {
- const spec: Spec = { root: "r", elements: {} };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "empty_spec")).toBe(true);
- });
- it("detects missing_child", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: { type: "Stack", props: {}, children: ["nonexistent"] },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "missing_child")).toBe(true);
- });
- it("detects visible_in_props", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Text",
- props: { visible: { $state: "/show" } },
- children: [],
- },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "visible_in_props")).toBe(true);
- });
- it("detects on_in_props", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Button",
- props: { on: { press: { action: "doSomething" } } },
- children: [],
- },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "on_in_props")).toBe(true);
- });
- it("detects repeat_in_props", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Stack",
- props: { repeat: { statePath: "/items" } },
- children: [],
- },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- expect(result.issues.some((i) => i.code === "repeat_in_props")).toBe(true);
- });
- it("detects watch_in_props", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Select",
- props: {
- watch: {
- "/form/country": { action: "loadCities" },
- },
- },
- children: [],
- },
- },
- };
- const result = validateSpec(spec);
- expect(result.valid).toBe(false);
- const watchIssue = result.issues.find((i) => i.code === "watch_in_props");
- expect(watchIssue).toBeDefined();
- expect(watchIssue!.elementKey).toBe("root");
- });
- it("detects orphaned elements when checkOrphans is true", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: { type: "Stack", props: {}, children: [] },
- orphan: { type: "Text", props: {}, children: [] },
- },
- };
- const result = validateSpec(spec, { checkOrphans: true });
- expect(result.valid).toBe(true);
- expect(result.issues.some((i) => i.code === "orphaned_element")).toBe(true);
- });
- });
- // =============================================================================
- // autoFixSpec
- // =============================================================================
- describe("autoFixSpec", () => {
- it("moves visible from props to element level", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Text",
- props: { text: "hi", visible: { $state: "/show" } },
- children: [],
- },
- },
- };
- const { spec: fixed, fixes } = autoFixSpec(spec);
- expect(
- (fixed.elements.root.props as Record<string, unknown>).visible,
- ).toBeUndefined();
- expect(fixed.elements.root.visible).toEqual({ $state: "/show" });
- expect(fixes.some((f) => f.includes("visible"))).toBe(true);
- });
- it("moves on from props to element level", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Button",
- props: { label: "OK", on: { press: { action: "submit" } } },
- children: [],
- },
- },
- };
- const { spec: fixed, fixes } = autoFixSpec(spec);
- expect(
- (fixed.elements.root.props as Record<string, unknown>).on,
- ).toBeUndefined();
- expect(fixed.elements.root.on).toEqual({ press: { action: "submit" } });
- expect(fixes.some((f) => f.includes('"on"'))).toBe(true);
- });
- it("moves repeat from props to element level", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Stack",
- props: { repeat: { statePath: "/items" } },
- children: ["child"],
- },
- child: { type: "Text", props: {}, children: [] },
- },
- };
- const { spec: fixed, fixes } = autoFixSpec(spec);
- expect(
- (fixed.elements.root.props as Record<string, unknown>).repeat,
- ).toBeUndefined();
- expect(fixed.elements.root.repeat).toEqual({ statePath: "/items" });
- expect(fixes.some((f) => f.includes('"repeat"'))).toBe(true);
- });
- it("moves watch from props to element level", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Select",
- props: {
- label: "Country",
- watch: {
- "/form/country": { action: "loadCities" },
- },
- },
- children: [],
- },
- },
- };
- const { spec: fixed, fixes } = autoFixSpec(spec);
- expect(
- (fixed.elements.root.props as Record<string, unknown>).watch,
- ).toBeUndefined();
- expect(fixed.elements.root.watch).toEqual({
- "/form/country": { action: "loadCities" },
- });
- expect(fixes.some((f) => f.includes('"watch"'))).toBe(true);
- });
- it("returns no fixes for a correct spec", () => {
- const spec: Spec = {
- root: "root",
- elements: {
- root: {
- type: "Stack",
- props: { direction: "vertical" },
- children: [],
- watch: { "/x": { action: "y" } },
- },
- },
- };
- const { fixes } = autoFixSpec(spec);
- expect(fixes).toHaveLength(0);
- });
- });
|