nextjs-hackathon-stack 0.1.10 → 0.1.12
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.
- package/dist/index.js +7 -0
- package/package.json +1 -1
- package/template/.husky/pre-commit +1 -0
- package/template/_env.example +7 -5
- package/template/package.json.tmpl +8 -2
- package/template/src/app/(protected)/page.tsx +9 -1
- package/template/src/app/__tests__/auth-callback.test.ts +72 -0
- package/template/src/app/__tests__/error.test.tsx +71 -0
- package/template/src/app/__tests__/layout.test.tsx +22 -0
- package/template/src/app/__tests__/login-page.test.tsx +34 -0
- package/template/src/app/__tests__/protected-layout.test.tsx +43 -0
- package/template/src/app/__tests__/protected-page.test.tsx +41 -0
- package/template/src/app/api/chat/route.ts +3 -1
- package/template/src/features/auth/__tests__/login-form.test.tsx +53 -2
- package/template/src/features/auth/__tests__/login.action.test.ts +82 -0
- package/template/src/features/auth/__tests__/logout.action.test.ts +32 -0
- package/template/src/features/auth/actions/login.action.ts +1 -1
- package/template/src/features/auth/components/login-form.tsx +4 -3
- package/template/src/features/chat/__tests__/chat-ui.test.tsx +97 -7
- package/template/src/features/chat/__tests__/route.test.ts +66 -3
- package/template/src/features/chat/__tests__/use-chat.test.ts +15 -0
- package/template/src/features/chat/components/chat-ui.tsx +1 -1
- package/template/src/features/tts/__tests__/route.test.ts +78 -3
- package/template/src/features/tts/__tests__/tts-player.test.tsx +65 -3
- package/template/src/features/video/__tests__/route.test.ts +78 -3
- package/template/src/features/video/__tests__/video-generator.test.tsx +65 -3
- package/template/src/shared/__tests__/middleware.test.ts +34 -0
- package/template/src/shared/__tests__/schema.test.ts +16 -3
- package/template/src/shared/__tests__/supabase-middleware.test.ts +162 -0
- package/template/src/shared/__tests__/supabase-server.test.ts +76 -3
- package/template/src/shared/__tests__/ui-button.test.tsx +52 -0
- package/template/src/shared/__tests__/ui-card.test.tsx +78 -0
- package/template/src/shared/__tests__/utils.test.ts +30 -0
- package/template/src/shared/lib/ai.ts +4 -4
- package/template/vitest.config.ts +5 -0
|
@@ -3,29 +3,119 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
3
3
|
|
|
4
4
|
import { ChatUi } from "../components/chat-ui";
|
|
5
5
|
|
|
6
|
+
const mockUseChat = vi.fn(() => ({
|
|
7
|
+
messages: [] as { id: string; role: string; content: string }[],
|
|
8
|
+
input: "",
|
|
9
|
+
handleInputChange: vi.fn(),
|
|
10
|
+
handleSubmit: vi.fn(),
|
|
11
|
+
status: "idle",
|
|
12
|
+
}));
|
|
13
|
+
|
|
6
14
|
vi.mock("@ai-sdk/react", () => ({
|
|
7
|
-
useChat:
|
|
8
|
-
messages: [],
|
|
9
|
-
input: "",
|
|
10
|
-
handleInputChange: vi.fn(),
|
|
11
|
-
handleSubmit: vi.fn(),
|
|
12
|
-
isLoading: false,
|
|
13
|
-
})),
|
|
15
|
+
useChat: (...args: unknown[]) => mockUseChat(...args),
|
|
14
16
|
}));
|
|
15
17
|
|
|
16
18
|
describe("ChatUi", () => {
|
|
17
19
|
it("renders chat input", () => {
|
|
20
|
+
// Arrange + Act
|
|
18
21
|
render(<ChatUi />);
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
19
24
|
expect(screen.getByTestId("chat-input")).toBeInTheDocument();
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
it("renders send button", () => {
|
|
28
|
+
// Arrange + Act
|
|
23
29
|
render(<ChatUi />);
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
24
32
|
expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument();
|
|
25
33
|
});
|
|
26
34
|
|
|
27
35
|
it("renders messages list area", () => {
|
|
36
|
+
// Arrange + Act
|
|
28
37
|
render(<ChatUi />);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
29
40
|
expect(screen.getByTestId("chat-ui")).toBeInTheDocument();
|
|
30
41
|
});
|
|
42
|
+
|
|
43
|
+
it("renders user and assistant messages", () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
mockUseChat.mockReturnValueOnce({
|
|
46
|
+
messages: [
|
|
47
|
+
{ id: "1", role: "user", content: "Hello" },
|
|
48
|
+
{ id: "2", role: "assistant", content: "Hi there!" },
|
|
49
|
+
],
|
|
50
|
+
input: "",
|
|
51
|
+
handleInputChange: vi.fn(),
|
|
52
|
+
handleSubmit: vi.fn(),
|
|
53
|
+
status: "idle",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
render(<ChatUi />);
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(screen.getByText("Hello")).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText("Hi there!")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("shows loading indicator when status is streaming", () => {
|
|
65
|
+
// Arrange
|
|
66
|
+
mockUseChat.mockReturnValueOnce({
|
|
67
|
+
messages: [],
|
|
68
|
+
input: "",
|
|
69
|
+
handleInputChange: vi.fn(),
|
|
70
|
+
handleSubmit: vi.fn(),
|
|
71
|
+
status: "streaming",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
render(<ChatUi />);
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(screen.getByText(/thinking/i)).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shows loading indicator when status is submitted", () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
mockUseChat.mockReturnValueOnce({
|
|
84
|
+
messages: [],
|
|
85
|
+
input: "",
|
|
86
|
+
handleInputChange: vi.fn(),
|
|
87
|
+
handleSubmit: vi.fn(),
|
|
88
|
+
status: "submitted",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
render(<ChatUi />);
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(screen.getByText(/thinking/i)).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("strips <think> tags from message content", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
mockUseChat.mockReturnValueOnce({
|
|
101
|
+
messages: [
|
|
102
|
+
{
|
|
103
|
+
id: "1",
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: "<think>internal reasoning</think>Visible answer",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
input: "",
|
|
109
|
+
handleInputChange: vi.fn(),
|
|
110
|
+
handleSubmit: vi.fn(),
|
|
111
|
+
status: "idle",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
render(<ChatUi />);
|
|
116
|
+
|
|
117
|
+
// Assert
|
|
118
|
+
expect(screen.getByText("Visible answer")).toBeInTheDocument();
|
|
119
|
+
expect(screen.queryByText(/internal reasoning/)).not.toBeInTheDocument();
|
|
120
|
+
});
|
|
31
121
|
});
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
let capturedGetErrorMessage: ((error: unknown) => string) | undefined;
|
|
4
|
+
|
|
3
5
|
vi.mock("ai", () => ({
|
|
4
6
|
streamText: vi.fn(() => ({
|
|
5
|
-
toDataStreamResponse: vi.fn((
|
|
7
|
+
toDataStreamResponse: vi.fn((opts?: { getErrorMessage?: (e: unknown) => string }) => {
|
|
8
|
+
capturedGetErrorMessage = opts?.getErrorMessage;
|
|
9
|
+
return new Response("data: done\n\n");
|
|
10
|
+
}),
|
|
6
11
|
})),
|
|
7
12
|
}));
|
|
8
13
|
|
|
@@ -11,9 +16,67 @@ vi.mock("@/shared/lib/ai", () => ({
|
|
|
11
16
|
}));
|
|
12
17
|
|
|
13
18
|
describe("chat route", () => {
|
|
14
|
-
it("
|
|
15
|
-
|
|
19
|
+
it("exports runtime as edge and POST handler", async () => {
|
|
20
|
+
// Arrange + Act
|
|
21
|
+
const mod = await import("../../../app/api/chat/route");
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
16
24
|
expect(mod.runtime).toBe("edge");
|
|
17
25
|
expect(typeof mod.POST).toBe("function");
|
|
18
26
|
});
|
|
27
|
+
|
|
28
|
+
it("calls streamText and returns a response", async () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const mod = await import("../../../app/api/chat/route");
|
|
31
|
+
const req = new Request("http://localhost/api/chat", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "hello" }] }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const response = await mod.POST(req);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(response).toBeInstanceOf(Response);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("getErrorMessage returns error.message for Error instances", async () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const mod = await import("../../../app/api/chat/route");
|
|
46
|
+
const req = new Request("http://localhost/api/chat", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ messages: [] }),
|
|
49
|
+
});
|
|
50
|
+
await mod.POST(req);
|
|
51
|
+
|
|
52
|
+
// Act + Assert
|
|
53
|
+
expect(capturedGetErrorMessage).toBeDefined();
|
|
54
|
+
expect(capturedGetErrorMessage!(new Error("boom"))).toBe("boom");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("getErrorMessage returns JSON for plain objects", async () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const mod = await import("../../../app/api/chat/route");
|
|
60
|
+
const req = new Request("http://localhost/api/chat", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({ messages: [] }),
|
|
63
|
+
});
|
|
64
|
+
await mod.POST(req);
|
|
65
|
+
|
|
66
|
+
// Act + Assert
|
|
67
|
+
expect(capturedGetErrorMessage!({ code: 42 })).toBe('{"code":42}');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("getErrorMessage returns String() for primitives", async () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
const mod = await import("../../../app/api/chat/route");
|
|
73
|
+
const req = new Request("http://localhost/api/chat", {
|
|
74
|
+
method: "POST",
|
|
75
|
+
body: JSON.stringify({ messages: [] }),
|
|
76
|
+
});
|
|
77
|
+
await mod.POST(req);
|
|
78
|
+
|
|
79
|
+
// Act + Assert
|
|
80
|
+
expect(capturedGetErrorMessage!("plain string")).toBe("plain string");
|
|
81
|
+
});
|
|
19
82
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@ai-sdk/react", () => ({
|
|
4
|
+
useChat: vi.fn(() => ({ messages: [], input: "" })),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
describe("use-chat re-export", () => {
|
|
8
|
+
it("re-exports useChat from @ai-sdk/react", async () => {
|
|
9
|
+
// Arrange + Act
|
|
10
|
+
const mod = await import("../hooks/use-chat");
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(typeof mod.useChat).toBe("function");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -18,7 +18,7 @@ export function ChatUi() {
|
|
|
18
18
|
message.role === "user" ? "bg-primary/10 ml-8" : "bg-muted mr-8"
|
|
19
19
|
}`}
|
|
20
20
|
>
|
|
21
|
-
<p className="text-sm">{message.content}</p>
|
|
21
|
+
<p className="text-sm">{message.content.replace(/<think>[\s\S]*?<\/think>\s*/g, "")}</p>
|
|
22
22
|
</div>
|
|
23
23
|
))}
|
|
24
24
|
{isLoading && (
|
|
@@ -1,13 +1,88 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockSynthesizeSpeech = vi.fn();
|
|
2
4
|
|
|
3
5
|
vi.mock("@/shared/lib/minimax-media", () => ({
|
|
4
|
-
synthesizeSpeech:
|
|
6
|
+
synthesizeSpeech: (...args: unknown[]) => mockSynthesizeSpeech(...args),
|
|
5
7
|
}));
|
|
6
8
|
|
|
7
9
|
describe("tts route", () => {
|
|
8
|
-
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("exports runtime as edge and POST handler", async () => {
|
|
15
|
+
// Arrange + Act
|
|
9
16
|
const mod = await import("../api/route");
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
10
19
|
expect(mod.runtime).toBe("edge");
|
|
11
20
|
expect(typeof mod.POST).toBe("function");
|
|
12
21
|
});
|
|
22
|
+
|
|
23
|
+
it("returns 400 when text is missing", async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const mod = await import("../api/route");
|
|
26
|
+
const req = new Request("http://localhost/api/tts", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const response = await mod.POST(req);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(response.status).toBe(400);
|
|
36
|
+
const body = await response.json() as { error: string };
|
|
37
|
+
expect(body.error).toBe("text is required");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns 400 when text is not a string", async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const mod = await import("../api/route");
|
|
43
|
+
const req = new Request("http://localhost/api/tts", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: JSON.stringify({ text: 123 }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const response = await mod.POST(req);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(response.status).toBe(400);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns audioFile on success", async () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
mockSynthesizeSpeech.mockResolvedValue("base64audiodata");
|
|
58
|
+
const mod = await import("../api/route");
|
|
59
|
+
const req = new Request("http://localhost/api/tts", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: JSON.stringify({ text: "Hello world" }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const response = await mod.POST(req);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(response.status).toBe(200);
|
|
69
|
+
const body = await response.json() as { audioFile: string };
|
|
70
|
+
expect(body.audioFile).toBe("base64audiodata");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("passes voiceId to synthesizeSpeech when provided", async () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
mockSynthesizeSpeech.mockResolvedValue("audio");
|
|
76
|
+
const mod = await import("../api/route");
|
|
77
|
+
const req = new Request("http://localhost/api/tts", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({ text: "Hello", voiceId: "custom-voice" }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
await mod.POST(req);
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
expect(mockSynthesizeSpeech).toHaveBeenCalledWith("Hello", "custom-voice");
|
|
87
|
+
});
|
|
13
88
|
});
|
|
@@ -1,21 +1,83 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
4
|
|
|
4
5
|
import { TtsPlayer } from "../components/tts-player";
|
|
5
6
|
|
|
6
7
|
describe("TtsPlayer", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
7
12
|
it("renders text input", () => {
|
|
13
|
+
// Arrange + Act
|
|
8
14
|
render(<TtsPlayer />);
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
9
17
|
expect(screen.getByPlaceholderText(/enter text to speak/i)).toBeInTheDocument();
|
|
10
18
|
});
|
|
11
19
|
|
|
12
20
|
it("renders speak button", () => {
|
|
21
|
+
// Arrange + Act
|
|
13
22
|
render(<TtsPlayer />);
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
14
25
|
expect(screen.getByRole("button", { name: /speak/i })).toBeInTheDocument();
|
|
15
26
|
});
|
|
16
27
|
|
|
17
|
-
it("button disabled when input empty", () => {
|
|
28
|
+
it("button is disabled when input is empty", () => {
|
|
29
|
+
// Arrange + Act
|
|
18
30
|
render(<TtsPlayer />);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
19
33
|
expect(screen.getByRole("button", { name: /speak/i })).toBeDisabled();
|
|
20
34
|
});
|
|
35
|
+
|
|
36
|
+
it("button is enabled when text has content", async () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(<TtsPlayer />);
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
await user.type(screen.getByPlaceholderText(/enter text to speak/i), "Hello world");
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(screen.getByRole("button", { name: /speak/i })).not.toBeDisabled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("shows audio player after successful synthesis", async () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
vi.spyOn(global, "fetch").mockResolvedValue(
|
|
51
|
+
new Response(JSON.stringify({ audioFile: "data:audio/mp3;base64,abc" }), { status: 200 })
|
|
52
|
+
);
|
|
53
|
+
const user = userEvent.setup();
|
|
54
|
+
render(<TtsPlayer />);
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
await user.type(screen.getByPlaceholderText(/enter text to speak/i), "Say something");
|
|
58
|
+
await user.click(screen.getByRole("button", { name: /speak/i }));
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByTestId("tts-player").querySelector("audio")).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("shows error message when synthesis fails", async () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
vi.spyOn(global, "fetch").mockResolvedValue(
|
|
69
|
+
new Response(null, { status: 500 })
|
|
70
|
+
);
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(<TtsPlayer />);
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
await user.type(screen.getByPlaceholderText(/enter text to speak/i), "Say something");
|
|
76
|
+
await user.click(screen.getByRole("button", { name: /speak/i }));
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(screen.getByText(/failed to synthesize speech/i)).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
21
83
|
});
|
|
@@ -1,13 +1,88 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockGenerateVideo = vi.fn();
|
|
2
4
|
|
|
3
5
|
vi.mock("@/shared/lib/minimax-media", () => ({
|
|
4
|
-
generateVideo:
|
|
6
|
+
generateVideo: (...args: unknown[]) => mockGenerateVideo(...args),
|
|
5
7
|
}));
|
|
6
8
|
|
|
7
9
|
describe("video route", () => {
|
|
8
|
-
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("exports runtime as edge and POST handler", async () => {
|
|
15
|
+
// Arrange + Act
|
|
9
16
|
const mod = await import("../api/route");
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
10
19
|
expect(mod.runtime).toBe("edge");
|
|
11
20
|
expect(typeof mod.POST).toBe("function");
|
|
12
21
|
});
|
|
22
|
+
|
|
23
|
+
it("returns 400 when prompt is missing", async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const mod = await import("../api/route");
|
|
26
|
+
const req = new Request("http://localhost/api/video", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const response = await mod.POST(req);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(response.status).toBe(400);
|
|
36
|
+
const body = await response.json() as { error: string };
|
|
37
|
+
expect(body.error).toBe("prompt is required");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns 400 when prompt is not a string", async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const mod = await import("../api/route");
|
|
43
|
+
const req = new Request("http://localhost/api/video", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: JSON.stringify({ prompt: 42 }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const response = await mod.POST(req);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(response.status).toBe(400);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns taskId on success", async () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
mockGenerateVideo.mockResolvedValue("task-123");
|
|
58
|
+
const mod = await import("../api/route");
|
|
59
|
+
const req = new Request("http://localhost/api/video", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: JSON.stringify({ prompt: "A sunset over the ocean" }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const response = await mod.POST(req);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(response.status).toBe(200);
|
|
69
|
+
const body = await response.json() as { taskId: string };
|
|
70
|
+
expect(body.taskId).toBe("task-123");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("passes prompt to generateVideo", async () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
mockGenerateVideo.mockResolvedValue("task-abc");
|
|
76
|
+
const mod = await import("../api/route");
|
|
77
|
+
const req = new Request("http://localhost/api/video", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({ prompt: "Dancing robots" }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
await mod.POST(req);
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
expect(mockGenerateVideo).toHaveBeenCalledWith("Dancing robots");
|
|
87
|
+
});
|
|
13
88
|
});
|
|
@@ -1,21 +1,83 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
4
|
|
|
4
5
|
import { VideoGenerator } from "../components/video-generator";
|
|
5
6
|
|
|
6
7
|
describe("VideoGenerator", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
7
12
|
it("renders prompt input", () => {
|
|
13
|
+
// Arrange + Act
|
|
8
14
|
render(<VideoGenerator />);
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
9
17
|
expect(screen.getByPlaceholderText(/describe a video/i)).toBeInTheDocument();
|
|
10
18
|
});
|
|
11
19
|
|
|
12
20
|
it("renders generate button", () => {
|
|
21
|
+
// Arrange + Act
|
|
13
22
|
render(<VideoGenerator />);
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
14
25
|
expect(screen.getByRole("button", { name: /generate/i })).toBeInTheDocument();
|
|
15
26
|
});
|
|
16
27
|
|
|
17
|
-
it("button disabled when input empty", () => {
|
|
28
|
+
it("button is disabled when input is empty", () => {
|
|
29
|
+
// Arrange + Act
|
|
18
30
|
render(<VideoGenerator />);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
19
33
|
expect(screen.getByRole("button", { name: /generate/i })).toBeDisabled();
|
|
20
34
|
});
|
|
35
|
+
|
|
36
|
+
it("button is enabled when prompt has content", async () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(<VideoGenerator />);
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
await user.type(screen.getByPlaceholderText(/describe a video/i), "A sunset");
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(screen.getByRole("button", { name: /generate/i })).not.toBeDisabled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("shows task ID after successful generation", async () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
vi.spyOn(global, "fetch").mockResolvedValue(
|
|
51
|
+
new Response(JSON.stringify({ taskId: "task-xyz" }), { status: 200 })
|
|
52
|
+
);
|
|
53
|
+
const user = userEvent.setup();
|
|
54
|
+
render(<VideoGenerator />);
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
await user.type(screen.getByPlaceholderText(/describe a video/i), "A dancing robot");
|
|
58
|
+
await user.click(screen.getByRole("button", { name: /generate/i }));
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText(/task id: task-xyz/i)).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("shows error message when fetch fails", async () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
vi.spyOn(global, "fetch").mockResolvedValue(
|
|
69
|
+
new Response(null, { status: 500 })
|
|
70
|
+
);
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(<VideoGenerator />);
|
|
73
|
+
|
|
74
|
+
// Act
|
|
75
|
+
await user.type(screen.getByPlaceholderText(/describe a video/i), "Something");
|
|
76
|
+
await user.click(screen.getByRole("button", { name: /generate/i }));
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(screen.getByText(/failed to generate video/i)).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
21
83
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
|
|
4
|
+
const mockUpdateSession = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock("@/shared/lib/supabase/middleware", () => ({
|
|
7
|
+
updateSession: (...args: unknown[]) => mockUpdateSession(...args),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("middleware", () => {
|
|
11
|
+
it("delegates to updateSession", async () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const mockResponse = new Response(null, { status: 200 });
|
|
14
|
+
mockUpdateSession.mockResolvedValue(mockResponse);
|
|
15
|
+
const { middleware } = await import("../../middleware");
|
|
16
|
+
const req = new NextRequest("http://localhost/dashboard");
|
|
17
|
+
|
|
18
|
+
// Act
|
|
19
|
+
const response = await middleware(req);
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(mockUpdateSession).toHaveBeenCalledWith(req);
|
|
23
|
+
expect(response).toBe(mockResponse);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("exports a matcher config", async () => {
|
|
27
|
+
// Arrange + Act
|
|
28
|
+
const { config } = await import("../../middleware");
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
expect(config.matcher).toBeDefined();
|
|
32
|
+
expect(Array.isArray(config.matcher)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -4,16 +4,29 @@ import { insertProfileSchema, selectProfileSchema } from "../db/schema";
|
|
|
4
4
|
|
|
5
5
|
describe("profiles schema", () => {
|
|
6
6
|
it("insertProfileSchema validates required fields", () => {
|
|
7
|
-
|
|
7
|
+
// Arrange + Act
|
|
8
|
+
const result = insertProfileSchema.safeParse({
|
|
9
|
+
id: "123e4567-e89b-12d3-a456-426614174000",
|
|
10
|
+
email: "test@example.com",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Assert
|
|
8
14
|
expect(result.success).toBe(true);
|
|
9
15
|
});
|
|
10
16
|
|
|
11
|
-
it("insertProfileSchema rejects invalid
|
|
12
|
-
|
|
17
|
+
it("insertProfileSchema rejects invalid uuid", () => {
|
|
18
|
+
// Arrange + Act
|
|
19
|
+
const result = insertProfileSchema.safeParse({
|
|
20
|
+
id: "not-a-uuid",
|
|
21
|
+
email: "test@example.com",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
13
25
|
expect(result.success).toBe(false);
|
|
14
26
|
});
|
|
15
27
|
|
|
16
28
|
it("selectProfileSchema is defined", () => {
|
|
29
|
+
// Arrange + Act + Assert
|
|
17
30
|
expect(selectProfileSchema).toBeDefined();
|
|
18
31
|
});
|
|
19
32
|
});
|