latency-curve.test.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /**
  2. * @vitest-environment happy-dom
  3. */
  4. import type { ReactNode } from "react";
  5. import { act } from "react";
  6. import { createRoot } from "react-dom/client";
  7. import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
  8. // Mock next-intl
  9. vi.mock("next-intl", () => ({
  10. useTranslations: () => (key: string) => key,
  11. useTimeZone: () => "UTC",
  12. }));
  13. // Mock recharts to expose color props via data-* attributes
  14. vi.mock("recharts", async (importOriginal) => {
  15. const React = await import("react");
  16. const actual = await importOriginal<typeof import("recharts")>();
  17. return {
  18. ...actual,
  19. ResponsiveContainer: ({ children }: { children: ReactNode }) =>
  20. React.createElement("div", { "data-testid": "recharts-responsive" }, children),
  21. LineChart: ({ children, data }: { children: ReactNode; data: unknown[] }) =>
  22. React.createElement(
  23. "div",
  24. { "data-testid": "recharts-linechart", "data-points": data?.length || 0 },
  25. children
  26. ),
  27. Line: ({
  28. stroke,
  29. dataKey,
  30. dot,
  31. activeDot,
  32. }: {
  33. stroke: string;
  34. dataKey: string;
  35. dot: unknown;
  36. activeDot: unknown;
  37. }) =>
  38. React.createElement("div", {
  39. "data-testid": `recharts-line-${dataKey}`,
  40. "data-stroke": stroke,
  41. "data-has-dot": dot != null ? "true" : "false",
  42. "data-activedot": JSON.stringify(activeDot),
  43. }),
  44. CartesianGrid: () => null,
  45. XAxis: () => null,
  46. YAxis: () => null,
  47. };
  48. });
  49. // Mock chart.tsx to expose ChartStyle for testing
  50. vi.mock("@/components/ui/chart", async () => {
  51. const React = await import("react");
  52. const actual =
  53. await vi.importActual<typeof import("@/components/ui/chart")>("@/components/ui/chart");
  54. return {
  55. ...actual,
  56. ChartContainer: ({
  57. children,
  58. config,
  59. className,
  60. }: {
  61. children: ReactNode;
  62. config: Record<string, { color?: string; label?: string }>;
  63. className?: string;
  64. }) =>
  65. React.createElement(
  66. "div",
  67. {
  68. "data-testid": "chart-container",
  69. "data-config": JSON.stringify(config),
  70. className,
  71. },
  72. children
  73. ),
  74. ChartTooltip: () => null,
  75. };
  76. });
  77. import { LatencyCurve } from "@/app/[locale]/dashboard/availability/_components/endpoint/latency-curve";
  78. import type { ProviderEndpointProbeLog } from "@/types/provider";
  79. const mockLogs: ProviderEndpointProbeLog[] = [
  80. {
  81. id: 1,
  82. endpointId: 1,
  83. ok: true,
  84. statusCode: 200,
  85. latencyMs: 120,
  86. createdAt: "2024-01-01T10:00:00Z",
  87. errorMessage: null,
  88. },
  89. {
  90. id: 2,
  91. endpointId: 1,
  92. ok: true,
  93. statusCode: 200,
  94. latencyMs: 150,
  95. createdAt: "2024-01-01T10:05:00Z",
  96. errorMessage: null,
  97. },
  98. {
  99. id: 3,
  100. endpointId: 1,
  101. ok: false,
  102. statusCode: 500,
  103. latencyMs: 200,
  104. createdAt: "2024-01-01T10:10:00Z",
  105. errorMessage: "Internal Server Error",
  106. },
  107. ];
  108. function renderComponent(logs: ProviderEndpointProbeLog[]) {
  109. const container = document.createElement("div");
  110. document.body.appendChild(container);
  111. const root = createRoot(container);
  112. act(() => {
  113. root.render(<LatencyCurve logs={logs} />);
  114. });
  115. return {
  116. container,
  117. unmount: () => {
  118. act(() => root.unmount());
  119. container.remove();
  120. },
  121. };
  122. }
  123. describe("LatencyCurve color bindings", () => {
  124. beforeEach(() => {
  125. document.body.innerHTML = "";
  126. });
  127. afterEach(() => {
  128. vi.clearAllMocks();
  129. });
  130. test("ChartConfig uses var(--chart-*) without hsl wrapper", () => {
  131. const { container, unmount } = renderComponent(mockLogs);
  132. const chartContainer = container.querySelector('[data-testid="chart-container"]');
  133. expect(chartContainer).toBeTruthy();
  134. const configStr = chartContainer?.getAttribute("data-config");
  135. expect(configStr).toBeTruthy();
  136. const config = JSON.parse(configStr!);
  137. // Color should use var(--chart-*) directly, NOT hsl(var(--primary)) or hsl(var(--chart-*))
  138. expect(config.latency.color).toBe("var(--chart-1)");
  139. // Ensure no hsl wrapper
  140. expect(config.latency.color).not.toMatch(/^hsl\(/);
  141. unmount();
  142. });
  143. test("Line stroke uses var(--color-latency) CSS variable", () => {
  144. const { container, unmount } = renderComponent(mockLogs);
  145. const line = container.querySelector('[data-testid="recharts-line-latency"]');
  146. expect(line).toBeTruthy();
  147. // Stroke should use var(--color-latency) injected by ChartContainer
  148. expect(line?.getAttribute("data-stroke")).toBe("var(--color-latency)");
  149. unmount();
  150. });
  151. test("renders no data message when logs are empty", () => {
  152. const { container, unmount } = renderComponent([]);
  153. // Should show no data message
  154. expect(container.textContent).toContain("noData");
  155. // Should not render chart
  156. expect(container.querySelector('[data-testid="chart-container"]')).toBeNull();
  157. unmount();
  158. });
  159. test("renders no data message when all logs have null latency", () => {
  160. const logsWithNullLatency: ProviderEndpointProbeLog[] = [
  161. {
  162. id: 1,
  163. endpointId: 1,
  164. ok: false,
  165. statusCode: null,
  166. latencyMs: null,
  167. createdAt: "2024-01-01T10:00:00Z",
  168. errorMessage: "Timeout",
  169. },
  170. ];
  171. const { container, unmount } = renderComponent(logsWithNullLatency);
  172. // Should show no data message
  173. expect(container.textContent).toContain("noData");
  174. // Should not render chart
  175. expect(container.querySelector('[data-testid="chart-container"]')).toBeNull();
  176. unmount();
  177. });
  178. });