diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml
index 199a881..ccd5972 100644
--- a/.forgejo/workflows/deploy.yaml
+++ b/.forgejo/workflows/deploy.yaml
@@ -8,12 +8,32 @@ on:
- 'backend/**'
jobs:
- deploy-backend-dev:
- # Usamos 'docker' porque é a label que seu runner atual possui
+ # Job 1: Run Backend Tests
+ test-backend:
runs-on: docker
+ container:
+ image: golang:1.24-alpine
+ steps:
+ - name: Checkout code
+ uses: https://github.com/actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ cd backend
+ go mod download
+
+ - name: Run tests
+ run: |
+ cd backend
+ go test -v ./internal/services/...
+ go test -v ./internal/core/usecases/...
+
+ # Job 2: Deploy (only if tests pass)
+ deploy-backend-dev:
+ runs-on: docker
+ needs: test-backend
steps:
- name: Executar Deploy via SSH na Apolo (Ambiente Dev)
- # Ajustado para usar a URL completa do GitHub para evitar o erro 'repository not found'
uses: https://github.com/appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
diff --git a/backend/internal/services/job_service_test.go b/backend/internal/services/job_service_test.go
index f6f3308..327ed36 100644
--- a/backend/internal/services/job_service_test.go
+++ b/backend/internal/services/job_service_test.go
@@ -35,7 +35,7 @@ func TestCreateJob(t *testing.T) {
},
mockRun: func() {
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
- WithArgs("1", "user-123", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()).
+ WithArgs("1", "user-123", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now()))
},
wantErr: false,
@@ -93,16 +93,16 @@ func TestGetJobs(t *testing.T) {
// List query
mock.ExpectQuery(regexp.QuoteMeta(`SELECT
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
- j.employment_type, j.work_mode, j.location, j.status, j.is_featured, j.created_at, j.updated_at,
+ j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at,
COALESCE(c.name, '') as company_name, c.logo_url as company_logo_url,
r.name as region_name, ci.name as city_name
FROM jobs j`)).
WillReturnRows(sqlmock.NewRows([]string{
"id", "company_id", "title", "description", "salary_min", "salary_max", "salary_type",
- "employment_type", "work_mode", "location", "status", "is_featured", "created_at", "updated_at",
+ "employment_type", "work_mode", "working_hours", "location", "status", "salary_negotiable", "is_featured", "created_at", "updated_at",
"company_name", "company_logo_url", "region_name", "city_name",
}).AddRow(
- "1", "10", "Dev", "Desc", 100, 200, "m", "ft", "Remote", "Remote", "open", false, time.Now(), time.Now(),
+ "1", "10", "Dev", "Desc", 100, 200, "m", "ft", "Remote", "40h", "Remote", "open", true, false, time.Now(), time.Now(),
"Acme", "url", "Region", "City",
))
diff --git a/backend/internal/services/ticket_service_test.go b/backend/internal/services/ticket_service_test.go
new file mode 100644
index 0000000..6597840
--- /dev/null
+++ b/backend/internal/services/ticket_service_test.go
@@ -0,0 +1,167 @@
+package services_test
+
+import (
+ "context"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/DATA-DOG/go-sqlmock"
+ "github.com/rede5/gohorsejobs/backend/internal/services"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateTicket(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+ }
+ defer db.Close()
+
+ service := services.NewTicketService(db)
+
+ tests := []struct {
+ name string
+ userID string
+ subject string
+ priority string
+ mockRun func()
+ wantErr bool
+ }{
+ {
+ name: "Success",
+ userID: "user-1",
+ subject: "Help me",
+ priority: "high",
+ mockRun: func() {
+ mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)).
+ WithArgs("user-1", "Help me", "high").
+ WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
+ AddRow("ticket-1", "user-1", "Help me", "open", "high", time.Now(), time.Now()))
+ },
+ wantErr: false,
+ },
+ {
+ name: "Default Priority",
+ userID: "user-1",
+ subject: "Help me",
+ priority: "",
+ mockRun: func() {
+ mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)).
+ WithArgs("user-1", "Help me", "medium").
+ WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
+ AddRow("ticket-1", "user-1", "Help me", "open", "medium", time.Now(), time.Now()))
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.mockRun()
+ got, err := service.CreateTicket(context.Background(), tt.userID, tt.subject, tt.priority)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("TicketService.CreateTicket() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr {
+ assert.Equal(t, "ticket-1", got.ID)
+ }
+ })
+ }
+}
+
+func TestListTickets(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+ }
+ defer db.Close()
+
+ service := services.NewTicketService(db)
+
+ tests := []struct {
+ name string
+ userID string
+ mockRun func()
+ wantErr bool
+ }{
+ {
+ name: "Success",
+ userID: "user-1",
+ mockRun: func() {
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets WHERE user_id = $1`)).
+ WithArgs("user-1").
+ WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
+ AddRow("ticket-1", "user-1", "Help", "open", "medium", time.Now(), time.Now()).
+ AddRow("ticket-2", "user-1", "Bug", "closed", "high", time.Now(), time.Now()))
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.mockRun()
+ tickets, err := service.ListTickets(context.Background(), tt.userID)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("TicketService.ListTickets() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr {
+ assert.Len(t, tickets, 2)
+ }
+ })
+ }
+}
+
+func TestGetTicket(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+ }
+ defer db.Close()
+
+ service := services.NewTicketService(db)
+
+ tests := []struct {
+ name string
+ ticketID string
+ userID string
+ mockRun func()
+ wantErr bool
+ }{
+ {
+ name: "Success",
+ ticketID: "ticket-1",
+ userID: "user-1",
+ mockRun: func() {
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets WHERE id = $1 AND user_id = $2`)).
+ WithArgs("ticket-1", "user-1").
+ WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
+ AddRow("ticket-1", "user-1", "Help", "open", "medium", time.Now(), time.Now()))
+
+ mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, ticket_id, user_id, message, created_at FROM ticket_messages WHERE ticket_id = $1`)).
+ WithArgs("ticket-1").
+ WillReturnRows(sqlmock.NewRows([]string{"id", "ticket_id", "user_id", "message", "created_at"}).
+ AddRow("msg-1", "ticket-1", "user-1", "Hello", time.Now()))
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.mockRun()
+ ticket, msgs, err := service.GetTicket(context.Background(), tt.ticketID, tt.userID)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("TicketService.GetTicket() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr {
+ assert.Equal(t, "ticket-1", ticket.ID)
+ assert.Len(t, msgs, 1)
+ }
+ })
+ }
+}
diff --git a/frontend/src/app/dashboard/users/page.test.tsx b/frontend/src/app/dashboard/users/page.test.tsx
new file mode 100644
index 0000000..11cee75
--- /dev/null
+++ b/frontend/src/app/dashboard/users/page.test.tsx
@@ -0,0 +1,137 @@
+import { render, screen, waitFor, fireEvent } from "@testing-library/react";
+import AdminUsersPage from "./page";
+import { usersApi, adminCompaniesApi } from "@/lib/api";
+import { getCurrentUser, isAdminUser } from "@/lib/auth";
+
+// Mocks
+jest.mock("next/navigation", () => ({
+ useRouter: () => ({ push: jest.fn() }),
+}));
+
+jest.mock("@/lib/api", () => ({
+ usersApi: {
+ list: jest.fn(),
+ create: jest.fn(),
+ update: jest.fn(),
+ delete: jest.fn(),
+ },
+ adminCompaniesApi: {
+ list: jest.fn(),
+ },
+}));
+
+jest.mock("@/lib/auth", () => ({
+ getCurrentUser: jest.fn(),
+ isAdminUser: jest.fn(),
+}));
+
+jest.mock("sonner", () => ({
+ toast: {
+ success: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+// UI Mocks
+jest.mock("@/components/ui/button", () => ({ Button: ({ children, onClick, ...props }: any) => {children} }));
+jest.mock("@/components/ui/input", () => ({ Input: (props: any) => }));
+jest.mock("@/components/ui/card", () => ({
+ Card: ({ children }: any) =>
{children}
,
+ CardHeader: ({ children }: any) => {children}
,
+ CardTitle: ({ children }: any) => {children} ,
+ CardDescription: ({ children }: any) => {children}
,
+ CardContent: ({ children }: any) => {children}
+}));
+jest.mock("@/components/ui/badge", () => ({ Badge: ({ children }: any) => {children} }));
+jest.mock("@/components/ui/table", () => ({
+ Table: ({ children }: any) => ,
+ TableHeader: ({ children }: any) => {children} ,
+ TableBody: ({ children }: any) => {children} ,
+ TableRow: ({ children }: any) => {children} ,
+ TableHead: ({ children }: any) => {children} ,
+ TableCell: ({ children }: any) => {children} ,
+}));
+jest.mock("@/components/ui/dialog", () => ({
+ Dialog: ({ children, open }: any) => open ? {children}
: null,
+ DialogContent: ({ children }: any) => {children}
,
+ DialogHeader: ({ children }: any) => {children}
,
+ DialogTitle: ({ children }: any) => {children} ,
+ DialogDescription: ({ children }: any) => {children}
,
+ DialogFooter: ({ children }: any) => {children}
,
+ DialogTrigger: ({ children, onClick }: any) => {children}
, // simplified
+}));
+jest.mock("@/components/ui/label", () => ({ Label: ({ children }: any) => {children} }));
+jest.mock("@/components/ui/select", () => ({
+ Select: ({ children, value, onValueChange }: any) => (
+ onValueChange(e.target.value)}>{children}
+ ),
+ SelectTrigger: ({ children }: any) => {children}
,
+ SelectValue: () => null,
+ SelectContent: ({ children }: any) => <>{children}>,
+ SelectItem: ({ children, value }: any) => {children} ,
+}));
+jest.mock("@/components/ui/skeleton", () => ({ Skeleton: () => Loading...
}));
+
+describe("AdminUsersPage", () => {
+ const mockUser = { id: "1", name: "Admin", role: "admin" };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (getCurrentUser as jest.Mock).mockReturnValue(mockUser);
+ (isAdminUser as jest.Mock).mockReturnValue(true);
+ (usersApi.list as jest.Mock).mockResolvedValue({
+ data: [
+ { id: "1", name: "John Doe", email: "john@example.com", role: "candidate", status: "active", created_at: new Date().toISOString() }
+ ],
+ pagination: { total: 1, page: 1, limit: 10 }
+ });
+ (adminCompaniesApi.list as jest.Mock).mockResolvedValue({ data: [] });
+ });
+
+ it("renders and lists users", async () => {
+ render( );
+
+ expect(screen.getByText("User management")).toBeInTheDocument();
+ // Loading state
+ expect(screen.getAllByText("Loading...").length).toBeGreaterThan(0);
+
+ await waitFor(() => {
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
+ expect(screen.getByText("john@example.com")).toBeInTheDocument();
+ });
+ });
+
+ it("opens create user dialog", async () => {
+ render( );
+
+ await waitFor(() => screen.getByText("New user"));
+
+ // Since DialogTrigger is mocked as div, fire click
+ // Note: In real Shadcn, DialogTrigger wraps button.
+ // We need to find the "New user" button which is inside DialogTrigger.
+ // Our mock wraps children.
+
+ fireEvent.click(screen.getByText("New user"));
+
+ // Check if dialog content appears
+ // The mock Dialog renders children if open.
+ // However, the button inside DialogTrigger usually toggles state in parent.
+ // But our mock Dialog just renders based on 'open' prop controlled by parent state.
+ // Wait, the parent component controls `isDialogOpen`.
+ // The real `DialogTrigger` calls `onOpenChange(true)`.
+ // Our mock `DialogTrigger` implementation `{ children, onClick }` is nice but `onClick` isn't passed from `DialogTrigger` prop in real usage?
+ // Actually `DialogTrigger` usually doesn't take onClick, it handles it internally.
+ // But we can't easily trigger the state change in the parent unless we simulate the real trigger behavior.
+ // Since we cannot rely on complex interaction in mock, we might skip "interactions" tests if mocks are too shallow.
+ // Or we assume `New user` button click works if we didn't mock Button to do nothing.
+ // But `Dialog` relies on `isDialogOpen` state.
+
+ // Let's rely on standard library behavior:
+ // The code has `
+
{/* Header */}
-
+
-
User management
-
Manage all platform users
+
User management
+
Manage all platform users
loadUsers()} disabled={loading}>
@@ -475,7 +475,7 @@ export default function AdminUsersPage() {
{/* Stats */}
-
+
Total users
@@ -530,80 +530,82 @@ export default function AdminUsersPage() {
) : (
-
-
-
- Name
- Email
- Role
- Status
- Created
- Actions
-
-
-
- {filteredUsers.length === 0 ? (
+
+
+
-
- No users found
-
+ Name
+ Email
+ Role
+ Status
+ Created
+ Actions
- ) : (
- filteredUsers.map((user) => (
-
- {user.name}
- {user.email}
- {getRoleBadge(user.role)}
-
-
- {user.status ? user.status.toUpperCase() : "UNKNOWN"}
-
-
-
- {user.created_at ? userDateFormatter.format(new Date(user.created_at)) : "-"}
-
-
-
-
handleView(user)}
- title="View Details"
- >
-
-
-
handleEdit(user)}
- disabled={user.role === "superadmin"}
- >
-
-
-
handleDeleteClick(user)}
- disabled={user.role === "superadmin"}
- >
-
-
-
+
+
+ {filteredUsers.length === 0 ? (
+
+
+ No users found
- ))
- )}
-
-
-
-
+ ) : (
+ filteredUsers.map((user) => (
+
+ {user.name}
+ {user.email}
+ {getRoleBadge(user.role)}
+
+
+ {user.status ? user.status.toUpperCase() : "UNKNOWN"}
+
+
+
+ {user.created_at ? userDateFormatter.format(new Date(user.created_at)) : "-"}
+
+
+
+
handleView(user)}
+ title="View Details"
+ >
+
+
+
handleEdit(user)}
+ disabled={user.role === "superadmin"}
+ >
+
+
+
handleDeleteClick(user)}
+ disabled={user.role === "superadmin"}
+ >
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
{totalUsers === 0
? "No users to display"
: `Showing ${(page - 1) * limit + 1}-${Math.min(page * limit, totalUsers)} of ${totalUsers}`}
diff --git a/frontend/src/app/forgot-password/page.test.tsx b/frontend/src/app/forgot-password/page.test.tsx
new file mode 100644
index 0000000..b49e6f1
--- /dev/null
+++ b/frontend/src/app/forgot-password/page.test.tsx
@@ -0,0 +1,51 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import ForgotPasswordPage from "./page";
+
+// Mock translation
+jest.mock("@/lib/i18n", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+// Mock simple UI components to avoid dependency issues if not configured
+jest.mock("@/components/ui/button", () => ({
+ Button: ({ children, ...props }: any) => {children} ,
+}));
+jest.mock("@/components/ui/card", () => ({
+ Card: ({ children }: any) => {children}
,
+ CardContent: ({ children }: any) => {children}
,
+}));
+jest.mock("@/components/ui/input", () => ({
+ Input: (props: any) => ,
+}));
+jest.mock("@/components/ui/label", () => ({
+ Label: ({ children, htmlFor }: any) => {children} ,
+}));
+jest.mock("@/components/ui/alert", () => ({
+ Alert: ({ children }: any) => {children}
,
+ AlertDescription: ({ children }: any) => {children}
,
+}));
+
+describe("ForgotPasswordPage", () => {
+ it("renders the form correctly", () => {
+ render( );
+
+ expect(screen.getByText("auth.forgot.title")).toBeInTheDocument();
+ expect(screen.getByText("auth.forgot.subtitle")).toBeInTheDocument();
+ expect(screen.getByLabelText("auth.forgot.fields.email")).toBeInTheDocument();
+ expect(screen.getByText("auth.forgot.submit")).toBeInTheDocument();
+ });
+
+ it("shows success message after submission", () => {
+ render( );
+
+ const emailInput = screen.getByLabelText("auth.forgot.fields.email");
+ fireEvent.change(emailInput, { target: { value: "test@example.com" } });
+
+ const submitBtn = screen.getByText("auth.forgot.submit");
+ fireEvent.click(submitBtn);
+
+ expect(screen.getByText("auth.forgot.success")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/app/forgot-password/page.tsx b/frontend/src/app/forgot-password/page.tsx
index d3fb836..27f3e71 100644
--- a/frontend/src/app/forgot-password/page.tsx
+++ b/frontend/src/app/forgot-password/page.tsx
@@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useTranslation } from "@/lib/i18n";
+import { ArrowLeft, Mail } from "lucide-react";
export default function ForgotPasswordPage() {
const { t } = useTranslation();
@@ -20,24 +21,42 @@ export default function ForgotPasswordPage() {
};
return (
-
-
+
+
+ {/* Back to Login - Mobile friendly top placement */}
+
+
+
+ {t("auth.forgot.backLogin")}
+
+
+
-
{t("auth.forgot.title")}
-
{t("auth.forgot.subtitle")}
+
+
+
+
{t("auth.forgot.title")}
+
{t("auth.forgot.subtitle")}
-
+
{submitted && (
-
- {t("auth.forgot.success")}
+
+
+ {t("auth.forgot.success")}
+
)}
-
+ {/* Back to Login - Desktop */}
+
+
{t("auth.forgot.backLogin")}
@@ -67,3 +89,4 @@ export default function ForgotPasswordPage() {
);
}
+
diff --git a/frontend/src/app/jobs/[id]/apply/page.tsx b/frontend/src/app/jobs/[id]/apply/page.tsx
index f842020..ef240fc 100644
--- a/frontend/src/app/jobs/[id]/apply/page.tsx
+++ b/frontend/src/app/jobs/[id]/apply/page.tsx
@@ -228,10 +228,10 @@ export default function JobApplicationPage({
-
+
{/* Header */}
-
+
{/* Step progress */}
-
+
Step {currentStep} of {steps.length}:{" "}
@@ -269,6 +269,25 @@ export default function JobApplicationPage({
+ {/* Step indicator - Mobile */}
+
+ {steps.map((step) => {
+ const isActive = step.id === currentStep;
+ const isCompleted = step.id < currentStep;
+ return (
+
+ );
+ })}
+
+
{/* Step indicator (desktop) */}
{steps.map((step) => {
@@ -602,17 +621,18 @@ export default function JobApplicationPage({
)}
-
+
Back
-
+
{isSubmitting ? (
"Submitting..."
diff --git a/frontend/src/app/jobs/[id]/page.test.tsx b/frontend/src/app/jobs/[id]/page.test.tsx
new file mode 100644
index 0000000..ec68a01
--- /dev/null
+++ b/frontend/src/app/jobs/[id]/page.test.tsx
@@ -0,0 +1,123 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import JobDetailPage from "./page";
+import { jobsApi } from "@/lib/api";
+
+// Mock React.use
+jest.mock("react", () => ({
+ ...jest.requireActual("react"),
+ use: (promise: Promise) => {
+ // A simple mock implementation that unwraps the resolved value
+ // This is a naive implementation for testing purposes
+ // In a real environment, we might need a better way to test 'use'
+ let status = "pending";
+ let result: any;
+ let suspender = promise.then(
+ (r) => {
+ status = "success";
+ result = r;
+ },
+ (e) => {
+ status = "error";
+ result = e;
+ }
+ );
+ // Since we are not in a real suspense boundary, we cheat a bit for sync usage in tests if possible
+ // But honestly, it's easier to just mock 'use' as a function that returns the value if the promise is already resolved
+ // or just pass a resolved object if we mocking the hook.
+ // However, since 'use' is imported from 'react', we can mock it.
+ return { id: "1" }; // simplified return for now matching param
+ },
+}));
+
+// Better approach: mock the component if we can't easily mock `use`
+// But we want to test the component.
+// Let's assume params is passed as a Promise.
+
+// Mock Next.js hooks
+jest.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: jest.fn(),
+ }),
+}));
+
+// Mock API
+jest.mock("@/lib/api", () => ({
+ jobsApi: {
+ getById: jest.fn(),
+ },
+}));
+
+// Mock components
+jest.mock("@/components/navbar", () => ({ Navbar: () => Navbar
}));
+jest.mock("@/components/footer", () => ({ Footer: () => Footer
}));
+jest.mock("@/components/ui/button", () => ({ Button: ({ children, ...props }: any) => {children} }));
+jest.mock("@/components/ui/card", () => ({
+ Card: ({ children }: any) => {children}
,
+ CardHeader: ({ children }: any) => {children}
,
+ CardTitle: ({ children }: any) => {children} ,
+ CardDescription: ({ children }: any) => {children}
,
+ CardContent: ({ children }: any) => {children}
,
+}));
+jest.mock("@/components/ui/badge", () => ({ Badge: ({ children }: any) => {children} }));
+jest.mock("@/components/ui/avatar", () => ({
+ Avatar: ({ children }: any) => {children}
,
+ AvatarFallback: ({ children }: any) => {children}
,
+ AvatarImage: ({ src }: any) => ,
+}));
+jest.mock("@/components/ui/separator", () => ({ Separator: () => }));
+
+// Mock framer-motion
+jest.mock("framer-motion", () => ({
+ motion: {
+ div: ({ children }: any) => {children}
,
+ }
+}));
+
+describe("JobDetailPage", () => {
+ const mockJob = {
+ id: "1",
+ title: "Senior Go Developer",
+ companyName: "GoHorse Inc",
+ description: "Great job",
+ location: "Remote",
+ salaryMin: 10000,
+ salaryMax: 20000,
+ createdAt: new Date().toISOString(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders job details when found", async () => {
+ (jobsApi.getById as jest.Mock).mockResolvedValue(mockJob);
+
+ // We can't easily test the `await params` unwrapping in simple Jest without complex setup.
+ // We will assume the mock of `React.use` works or we pass a resolved promise?
+ // Actually, `use` is a hook.
+
+ // Simplification: We will rendering the component.
+ // However, `use(params)` inside the component will call our mocked `use`.
+
+ const paramsPromise = Promise.resolve({ id: "1" });
+ render( );
+
+ expect(screen.getByText("Loading job...")).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(screen.queryByText("Loading job...")).not.toBeInTheDocument();
+ expect(screen.getByText("Senior Go Developer")).toBeInTheDocument();
+ expect(screen.getByText("GoHorse Inc")).toBeInTheDocument();
+ });
+ });
+
+ it("renders not found state", async () => {
+ (jobsApi.getById as jest.Mock).mockResolvedValue(null);
+ const paramsPromise = Promise.resolve({ id: "999" });
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText("Job not found")).toBeInTheDocument();
+ });
+ });
+});