diff --git a/tests/contact.test.tsx b/tests/contact.test.tsx
new file mode 100644
index 0000000..0b80622
--- /dev/null
+++ b/tests/contact.test.tsx
@@ -0,0 +1,129 @@
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { NextIntlClientProvider } from "next-intl";
+import messages from "../messages/de.json";
+
+// Mocks MUST be defined before component import to ensure they are picked up
+vi.mock("../components/Reveal", () => ({
+ Reveal: ({ children }: any) => <>{children}>,
+ Stagger: ({ children }: any) => <>{children}>,
+}));
+
+// Better FormData mock for happy-dom
+global.FormData = class MockFormData {
+ private data = new Map();
+ constructor(form?: HTMLFormElement) {
+ if (form) {
+ const elements = form.elements as any;
+ for (let i = 0; i < elements.length; i++) {
+ const item = elements.item(i);
+ if (item.name && item.value) {
+ this.data.set(item.name, item.value);
+ }
+ }
+ }
+ }
+ append(key: string, value: any) {
+ this.data.set(key, value);
+ }
+ get(key: string) {
+ return this.data.get(key);
+ }
+ entries() {
+ return Array.from(this.data.entries())[Symbol.iterator]();
+ }
+} as any;
+
+// Mock alert
+const alertMock = vi.fn();
+global.alert = alertMock;
+
+// Import component AFTER mocks
+import Contact from "../components/ContactContent";
+
+// Mock fetch
+const fetchMock = vi.fn();
+global.fetch = fetchMock;
+
+const renderContact = () => {
+ return render(
+
+
+ ,
+ );
+};
+
+describe("Contact Page", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ fetchMock.mockReset();
+ fetchMock.mockResolvedValue({
+ ok: true,
+ json: async () => ({ success: true }),
+ });
+ });
+
+ it("renders the contact form correctly", () => {
+ renderContact();
+
+ expect(screen.getByLabelText(/Name \*/i)).toBeInTheDocument();
+ expect(screen.getByLabelText(/Firma/i)).toBeInTheDocument();
+ expect(screen.getByLabelText(/E-Mail \*/i)).toBeInTheDocument();
+ expect(screen.getByLabelText(/Nachricht \*/i)).toBeInTheDocument();
+ });
+
+ it("submits the form successfully", async () => {
+ renderContact();
+
+ fireEvent.change(screen.getByLabelText(/Name \*/i), {
+ target: { value: "John Doe" },
+ });
+ fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
+ target: { value: "john@example.com" },
+ });
+ fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
+ target: { value: "This is a test message that is long enough." },
+ });
+
+ const form = screen.getByRole("form");
+ fireEvent.submit(form);
+
+ await waitFor(
+ () => {
+ expect(fetchMock).toHaveBeenCalled();
+ },
+ { timeout: 2000 },
+ );
+
+ expect(
+ (await screen.findAllByText(/Anfrage erfolgreich übermittelt/i)).length,
+ ).toBeGreaterThanOrEqual(1);
+ expect(
+ (await screen.findAllByText(/Ihr Anliegen wurde erfasst/i)).length,
+ ).toBeGreaterThanOrEqual(1);
+ });
+
+ it("handles submission errors", async () => {
+ fetchMock.mockResolvedValueOnce({
+ ok: false,
+ json: async () => ({ error: "Server error" }),
+ });
+
+ renderContact();
+
+ fireEvent.change(screen.getByLabelText(/Name \*/i), {
+ target: { value: "John Doe" },
+ });
+ fireEvent.change(screen.getByLabelText(/E-Mail \*/i), {
+ target: { value: "john@example.com" },
+ });
+ fireEvent.change(screen.getByLabelText(/Nachricht \*/i), {
+ target: { value: "This is a test message." },
+ });
+
+ const form = screen.getByRole("form");
+ fireEvent.submit(form);
+
+ expect(await screen.findByText(/Server error/i)).toBeInTheDocument();
+ });
+});
diff --git a/tests/home.test.tsx b/tests/home.test.tsx
new file mode 100644
index 0000000..212b687
--- /dev/null
+++ b/tests/home.test.tsx
@@ -0,0 +1,37 @@
+import { render, screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import Home from "../components/HomeContent";
+import { NextIntlClientProvider } from "next-intl";
+import messages from "../messages/de.json";
+
+const renderHome = () => {
+ return render(
+
+
+ ,
+ );
+};
+
+describe("Home Page", () => {
+ it("renders the hero section with correct title", () => {
+ renderHome();
+ expect(
+ screen.getByRole("heading", { name: /Spezialisierter Partner/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("contains the CTA button", () => {
+ renderHome();
+ const ctaButton = screen.getByRole("link", { name: /Projekt anfragen/i });
+ expect(ctaButton).toBeInTheDocument();
+ expect(ctaButton).toHaveAttribute("href", "/kontakt");
+ });
+
+ it("renders the portfolio section", async () => {
+ renderHome();
+ expect(await screen.findByText(/Unsere Leistungen/i)).toBeInTheDocument();
+ // Use getAllByText because it appears in both hero description and card title
+ const elements = await screen.findAllByText(/Technische Beratung/i);
+ expect(elements.length).toBeGreaterThan(0);
+ });
+});
diff --git a/tests/setup.tsx b/tests/setup.tsx
new file mode 100644
index 0000000..3f5541b
--- /dev/null
+++ b/tests/setup.tsx
@@ -0,0 +1,56 @@
+import "@testing-library/jest-dom/vitest";
+import React from "react";
+import { vi } from "vitest";
+
+// Mock next/navigation
+vi.mock("next/navigation", () => ({
+ usePathname: () => "/",
+ useRouter: () => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+ back: vi.fn(),
+ }),
+ useSearchParams: () => new URLSearchParams(),
+}));
+
+// Mock next-intl to avoid transitive next/server issues
+vi.mock("next-intl/middleware", () => ({
+ default: vi.fn(() => (req: any) => req),
+}));
+
+vi.mock("next-intl/server", () => ({
+ getRequestConfig: vi.fn(),
+}));
+
+// Mock next/server
+vi.mock("next/server", () => ({
+ NextResponse: {
+ json: vi.fn(),
+ next: vi.fn(),
+ redirect: vi.fn(),
+ },
+}));
+
+// Mock next/dynamic to be synchronous in tests
+vi.mock("next/dynamic", () => ({
+ default: vi.fn((loader) => {
+ return (props: any) => {
+ const [Component, setComponent] = React.useState(null);
+ React.useEffect(() => {
+ loader().then((mod: any) => {
+ setComponent(
+ () =>
+ mod.default ||
+ mod.PortfolioSection ||
+ mod.ExpertiseSection ||
+ mod.TechnicalSpecsSection ||
+ mod.CTASection ||
+ mod,
+ );
+ });
+ }, []);
+ return Component ? : null;
+ };
+ }),
+}));