directives.test.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { describe, it, expect } from "vitest";
  2. import React from "react";
  3. import { render, screen } from "@testing-library/react";
  4. import { z } from "zod";
  5. import type { Spec } from "@json-render/core";
  6. import { defineDirective, resolvePropValue } from "@json-render/core";
  7. import {
  8. JSONUIProvider,
  9. Renderer,
  10. type ComponentRenderProps,
  11. } from "./renderer";
  12. function Text({ element }: ComponentRenderProps<{ text: unknown }>) {
  13. const value = element.props.text;
  14. return <span data-testid="text">{value == null ? "" : String(value)}</span>;
  15. }
  16. const registry = { Text };
  17. const doubleDirective = defineDirective({
  18. name: "$double",
  19. description: "Double a numeric value.",
  20. schema: z.object({ $double: z.unknown() }),
  21. resolve(value, ctx) {
  22. const resolved = resolvePropValue(value.$double, ctx);
  23. return (resolved as number) * 2;
  24. },
  25. });
  26. const upperDirective = defineDirective({
  27. name: "$upper",
  28. schema: z.object({ $upper: z.unknown() }),
  29. resolve(value, ctx) {
  30. const resolved = resolvePropValue(value.$upper, ctx);
  31. return String(resolved).toUpperCase();
  32. },
  33. });
  34. describe("directives in React renderer", () => {
  35. it("resolves a custom directive in rendered props", () => {
  36. const spec: Spec = {
  37. root: "main",
  38. elements: {
  39. main: {
  40. type: "Text",
  41. props: { text: { $upper: "hello" } },
  42. children: [],
  43. },
  44. },
  45. };
  46. render(
  47. <JSONUIProvider
  48. registry={registry}
  49. directives={[doubleDirective, upperDirective]}
  50. >
  51. <Renderer spec={spec} registry={registry} />
  52. </JSONUIProvider>,
  53. );
  54. expect(screen.getByTestId("text").textContent).toBe("HELLO");
  55. });
  56. it("resolves a directive that reads from state", () => {
  57. const spec: Spec = {
  58. root: "main",
  59. elements: {
  60. main: {
  61. type: "Text",
  62. props: { text: { $double: { $state: "/count" } } },
  63. children: [],
  64. },
  65. },
  66. state: { count: 7 },
  67. };
  68. render(
  69. <JSONUIProvider
  70. registry={registry}
  71. initialState={{ count: 7 }}
  72. directives={[doubleDirective, upperDirective]}
  73. >
  74. <Renderer spec={spec} registry={registry} />
  75. </JSONUIProvider>,
  76. );
  77. expect(screen.getByTestId("text").textContent).toBe("14");
  78. });
  79. it("renders without directives (backward compat)", () => {
  80. const spec: Spec = {
  81. root: "main",
  82. elements: {
  83. main: {
  84. type: "Text",
  85. props: { text: "plain" },
  86. children: [],
  87. },
  88. },
  89. };
  90. render(
  91. <JSONUIProvider registry={registry}>
  92. <Renderer spec={spec} registry={registry} />
  93. </JSONUIProvider>,
  94. );
  95. expect(screen.getByTestId("text").textContent).toBe("plain");
  96. });
  97. });