@carlonicora/nextjs-jsonapi 1.60.0 → 1.61.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import { EditorSheet } from "../EditorSheet";
8
+
9
+ // Mock next-intl
10
+ vi.mock("next-intl", () => ({
11
+ useTranslations: () => {
12
+ const t = (key: string, params?: Record<string, string>) => {
13
+ if (params?.type) return `${key}:${params.type}`;
14
+ return key;
15
+ };
16
+ return t;
17
+ },
18
+ }));
19
+
20
+ // Mock usePageUrlGenerator
21
+ vi.mock("../../../hooks/usePageUrlGenerator", () => ({
22
+ usePageUrlGenerator: () => (params: { page?: any; id?: string }) => `/test/${params.id || ""}`,
23
+ }));
24
+
25
+ // Mock errorToast
26
+ vi.mock("../../errors/errorToast", () => ({
27
+ errorToast: vi.fn(),
28
+ }));
29
+
30
+ const testModule = { pageUrl: "/test", name: "tests" } as any;
31
+
32
+ const schema = z.object({
33
+ name: z.string().min(1),
34
+ });
35
+
36
+ function TestEditor({
37
+ onSubmit = vi.fn().mockResolvedValue({ id: "123" }),
38
+ isEdit = false,
39
+ ...props
40
+ }: Partial<React.ComponentProps<typeof EditorSheet>> & { onSubmit?: any }) {
41
+ const form = useForm({
42
+ resolver: zodResolver(schema),
43
+ defaultValues: { name: "" },
44
+ });
45
+
46
+ return (
47
+ <EditorSheet
48
+ form={form}
49
+ onSubmit={onSubmit}
50
+ onReset={() => ({ name: "" })}
51
+ entityType="Test Entity"
52
+ isEdit={isEdit}
53
+ module={testModule}
54
+ {...props}
55
+ >
56
+ <input {...form.register("name")} data-testid="name-input" />
57
+ </EditorSheet>
58
+ );
59
+ }
60
+
61
+ describe("EditorSheet", () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ describe("trigger rendering", () => {
67
+ it("should render create button when not editing", () => {
68
+ render(<TestEditor />);
69
+ expect(screen.getByText("ui.buttons.create")).toBeTruthy();
70
+ });
71
+
72
+ it("should render custom trigger when provided", () => {
73
+ render(<TestEditor trigger={<button>Custom</button>} />);
74
+ expect(screen.getByText("Custom")).toBeTruthy();
75
+ });
76
+
77
+ it("should not render trigger when dialogOpen is controlled", () => {
78
+ render(<TestEditor dialogOpen={false} />);
79
+ expect(screen.queryByText("ui.buttons.create")).toBeNull();
80
+ });
81
+ });
82
+
83
+ describe("sheet header", () => {
84
+ it("should show create title when not editing", () => {
85
+ render(<TestEditor dialogOpen={true} />);
86
+ expect(screen.getByText("common.edit.create.title:Test Entity")).toBeTruthy();
87
+ });
88
+
89
+ it("should show update title when editing", () => {
90
+ render(<TestEditor dialogOpen={true} isEdit={true} entityName="My Item" />);
91
+ expect(screen.getByText("common.edit.update.title:Test Entity")).toBeTruthy();
92
+ });
93
+ });
94
+
95
+ describe("size presets", () => {
96
+ it("should default to xl size", () => {
97
+ render(<TestEditor dialogOpen={true} />);
98
+ const content = document.querySelector("[data-side]");
99
+ expect(content?.className).toContain("max-w-7xl");
100
+ });
101
+
102
+ it("should apply sm size", () => {
103
+ render(<TestEditor dialogOpen={true} size="sm" />);
104
+ const content = document.querySelector("[data-side]");
105
+ expect(content?.className).toContain("max-w-2xl");
106
+ });
107
+ });
108
+
109
+ describe("submit flow", () => {
110
+ it("should call onSubmit with form values on submit", async () => {
111
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
112
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} />);
113
+
114
+ const input = screen.getByTestId("name-input");
115
+ await userEvent.type(input, "Test Name");
116
+
117
+ const submitButton = screen.getByTestId("modal-button-create");
118
+ await userEvent.click(submitButton);
119
+
120
+ expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ name: "Test Name" }));
121
+ });
122
+
123
+ it("should call onSuccess instead of navigation when provided", async () => {
124
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
125
+ const onSuccess = vi.fn();
126
+ const onNavigate = vi.fn();
127
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onSuccess={onSuccess} onNavigate={onNavigate} />);
128
+
129
+ const input = screen.getByTestId("name-input");
130
+ await userEvent.type(input, "Test Name");
131
+
132
+ const submitButton = screen.getByTestId("modal-button-create");
133
+ await userEvent.click(submitButton);
134
+
135
+ expect(onSuccess).toHaveBeenCalled();
136
+ expect(onNavigate).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("should call onNavigate when onSuccess is not provided", async () => {
140
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
141
+ const onNavigate = vi.fn();
142
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onNavigate={onNavigate} />);
143
+
144
+ const input = screen.getByTestId("name-input");
145
+ await userEvent.type(input, "Test Name");
146
+
147
+ const submitButton = screen.getByTestId("modal-button-create");
148
+ await userEvent.click(submitButton);
149
+
150
+ expect(onNavigate).toHaveBeenCalledWith("/test/abc");
151
+ });
152
+
153
+ it("should call onRevalidate on successful submit", async () => {
154
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
155
+ const onRevalidate = vi.fn();
156
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onRevalidate={onRevalidate} />);
157
+
158
+ const input = screen.getByTestId("name-input");
159
+ await userEvent.type(input, "Test Name");
160
+
161
+ const submitButton = screen.getByTestId("modal-button-create");
162
+ await userEvent.click(submitButton);
163
+
164
+ expect(onRevalidate).toHaveBeenCalled();
165
+ });
166
+
167
+ it("should call propagateChanges instead of onNavigate when editing", async () => {
168
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc", name: "Updated" });
169
+ const propagateChanges = vi.fn();
170
+ const onNavigate = vi.fn();
171
+ render(
172
+ <TestEditor
173
+ dialogOpen={true}
174
+ isEdit={true}
175
+ onSubmit={onSubmit}
176
+ propagateChanges={propagateChanges}
177
+ onNavigate={onNavigate}
178
+ />,
179
+ );
180
+
181
+ const input = screen.getByTestId("name-input");
182
+ await userEvent.type(input, "Test Name");
183
+
184
+ const submitButton = screen.getByTestId("modal-button-create");
185
+ await userEvent.click(submitButton);
186
+
187
+ expect(propagateChanges).toHaveBeenCalledWith({ id: "abc", name: "Updated" });
188
+ expect(onNavigate).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it("should show error toast on submit failure", async () => {
192
+ const { errorToast: mockErrorToast } = await import("../../errors/errorToast");
193
+ const error = new Error("Network error");
194
+ const onSubmit = vi.fn().mockRejectedValue(error);
195
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} />);
196
+
197
+ const input = screen.getByTestId("name-input");
198
+ await userEvent.type(input, "Test Name");
199
+
200
+ const submitButton = screen.getByTestId("modal-button-create");
201
+ await userEvent.click(submitButton);
202
+
203
+ expect(mockErrorToast).toHaveBeenCalledWith({
204
+ title: "generic.errors.create",
205
+ error,
206
+ });
207
+ });
208
+ });
209
+
210
+ describe("disabled prop", () => {
211
+ it("should disable submit button when disabled prop is true", () => {
212
+ render(<TestEditor dialogOpen={true} disabled={true} />);
213
+ const submitButton = screen.getByTestId("modal-button-create");
214
+ expect(submitButton).toBeDisabled();
215
+ });
216
+ });
217
+ });
@@ -5,6 +5,7 @@ export * from "./CommonEditorHeader";
5
5
  export * from "./CommonEditorDiscardDialog";
6
6
  export * from "./CommonEditorTrigger";
7
7
  export * from "./useEditorDialog";
8
+ export * from "./EditorSheet";
8
9
  export * from "./DatePickerPopover";
9
10
  export * from "./DateRangeSelector";
10
11
  export * from "./FileUploader";
@@ -53,7 +53,7 @@ function SheetContent({
53
53
  data-slot="sheet-content"
54
54
  data-side={side}
55
55
  className={cn(
56
- "bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
56
+ "bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:rounded-l-lg data-[side=right]:border-l data-[side=left]:rounded-r-lg data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
57
57
  className,
58
58
  )}
59
59
  {...props}