diff --git a/frontend/jest.config.js b/frontend/jest.config.js index a7a6340..172130f 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -12,6 +12,7 @@ const customJestConfig = { moduleNameMapper: { '^@/(.*)$': '/src/$1', }, + testPathIgnorePatterns: ['/node_modules/', '/.next/', '/e2e/'], } // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/frontend/src/app/jobs/[id]/apply/page.test.tsx b/frontend/src/app/jobs/[id]/apply/page.test.tsx new file mode 100644 index 0000000..cd71c55 --- /dev/null +++ b/frontend/src/app/jobs/[id]/apply/page.test.tsx @@ -0,0 +1,149 @@ + +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import JobApplicationPage from "./page"; +import { jobsApi, applicationsApi } from "@/lib/api"; +import { useNotify } from "@/contexts/notification-context"; +import { useRouter } from "next/navigation"; + +// Mock dependencies +jest.mock("@/lib/i18n", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + locale: "en", + setLocale: jest.fn(), + }), + useI18n: () => ({ + t: (key: string) => key, + locale: "en", + setLocale: jest.fn(), + }), +})); + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), +})); + +jest.mock("@/lib/api", () => ({ + jobsApi: { + getById: jest.fn(), + }, + applicationsApi: { + create: jest.fn(), + }, +})); + +jest.mock("@/contexts/notification-context", () => ({ + useNotify: jest.fn(), +})); + +jest.mock("@/components/ui/progress", () => ({ + Progress: () =>
, +})); + + +describe("JobApplicationPage", () => { + const mockPush = jest.fn(); + const mockNotify = { error: jest.fn(), success: jest.fn(), info: jest.fn() }; + const mockJob = { + id: "job-123", + title: "Software Engineer", + companyName: "Tech Corp", + location: "Remote", + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useRouter as jest.Mock).mockReturnValue({ push: mockPush }); + (useNotify as jest.Mock).mockReturnValue(mockNotify); + (jobsApi.getById as jest.Mock).mockResolvedValue(mockJob); + window.scrollTo = jest.fn(); + }); + + // Since component uses `use(params)`, we need to wrap or pass a promise + const params = Promise.resolve({ id: "job-123" }); + + it("renders job title and company info", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Application: Software Engineer")).toBeInTheDocument(); + expect(screen.getByText(/Tech Corp/)).toBeInTheDocument(); + }); + }); + + it("validates step 1 fields", async () => { + render(); + + await waitFor(() => expect(screen.getByText("Application: Software Engineer")).toBeInTheDocument()); + + // Try next empty + const nextBtn = screen.getByRole("button", { name: /Next step/i }); + fireEvent.click(nextBtn); + expect(mockNotify.error).toHaveBeenCalledWith("Required fields", expect.any(String)); + + // Fill fields + fireEvent.change(screen.getByLabelText(/Full name/i), { target: { value: "John Doe" } }); + fireEvent.change(screen.getByLabelText(/Email/i), { target: { value: "john@example.com" } }); + fireEvent.change(screen.getByLabelText(/Phone/i), { target: { value: "11999999999" } }); + + // Forgot checkbox + fireEvent.click(nextBtn); + expect(mockNotify.error).toHaveBeenCalledWith("Privacy policy", expect.any(String)); + + // Check checkbox + // Finding checkbox by ID might be easiest or by label text click + const policyLabel = screen.getByText(/I have read and agree to the/i); + fireEvent.click(policyLabel); // Should toggle checkbox associated with label + + fireEvent.click(nextBtn); + + // Should move to step 2 + expect(screen.getByText("Resume & Documents")).toBeInTheDocument(); + }); + + it("submits application successfully", async () => { + render(); + await waitFor(() => expect(jobsApi.getById).toHaveBeenCalled()); + + // Step 1 + fireEvent.change(screen.getByLabelText(/Full name/i), { target: { value: "John Doe" } }); + fireEvent.change(screen.getByLabelText(/Email/i), { target: { value: "john@example.com" } }); + fireEvent.change(screen.getByLabelText(/Phone/i), { target: { value: "11999999999" } }); + fireEvent.click(screen.getByText(/I have read and agree to the/i)); + fireEvent.click(screen.getByRole("button", { name: /Next step/i })); + + // Step 2 + // Optional fields here or skip + // Resume is technically required (Step 2 logic: case 2 returns true currently in the simplified code? + // Looking at source: case 2: return true; (lines 122). So no validation on Step 2!) + fireEvent.click(screen.getByRole("button", { name: /Next step/i })); + + // Step 3 + // Salary and Experience + const trigger = screen.getByText("Select a range"); + fireEvent.click(trigger); + fireEvent.click(screen.getByText("Up to R$ 3,000")); // SelectOption + + fireEvent.click(screen.getByLabelText("Yes, I do")); // Radio + + fireEvent.click(screen.getByRole("button", { name: /Next step/i })); + + // Step 4 + // Why Us and Availability + fireEvent.change(screen.getByLabelText(/Why do you want to work/i), { target: { value: "Because I like it." } }); + fireEvent.click(screen.getByLabelText("Immediate start")); + + // Submit + fireEvent.click(screen.getByRole("button", { name: /Submit application/i })); + + await waitFor(() => { + expect(applicationsApi.create).toHaveBeenCalledWith(expect.objectContaining({ + name: "John Doe", + email: "john@example.com", + jobId: "job-123" + })); + expect(mockNotify.success).toHaveBeenCalled(); + expect(mockPush).toHaveBeenCalledWith("/dashboard/my-applications"); + }); + }); +}); diff --git a/frontend/src/app/post-job/page.test.tsx b/frontend/src/app/post-job/page.test.tsx new file mode 100644 index 0000000..a156d31 --- /dev/null +++ b/frontend/src/app/post-job/page.test.tsx @@ -0,0 +1,197 @@ + +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import PostJobPage from "./page"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; + +// Mock dependencies +jest.mock("@/lib/i18n", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + locale: "en", + setLocale: jest.fn(), + }), + useI18n: () => ({ + t: (key: string) => key, + locale: "en", + setLocale: jest.fn(), + }), +})); + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), +})); + +jest.mock("sonner", () => ({ + toast: { + error: jest.fn(), + success: jest.fn(), + }, +})); + +jest.mock("./translations", () => ({ + translations: { + pt: { + title: "Postar uma Vaga", + buttons: { next: "Next Step", publish: "Publish Job", publishing: "Publishing..." }, + company: { + name: "Company Name", + namePlaceholder: "My Company", + email: "Email", + password: "Password", + confirmPassword: "Confirm Password", + phone: "Phone", + }, + job: { + title: "Job Details", + jobTitle: "Job Title", + jobTitlePlaceholder: "e.g. Developer", + description: "Job Description", + location: "Location", + salary: "Salary", + }, + steps: { data: "Data", confirm: "Confirm" }, + cardTitle: { step1: "Step 1", step2: "Step 2" }, + cardDesc: { step1: "Desc 1", step2: "Desc 2" }, + common: { company: "Company", job: "Job", name: "Name", email: "Email", title: "Title" }, + options: { + period: { hourly: "/hr" }, + contract: { permanent: "Permanent" }, + hours: { fullTime: "Full Time" }, + mode: { remote: "Remote" } + } + }, + }, +})); + +// Mock RichTextEditor component +jest.mock("@/components/rich-text-editor", () => ({ + RichTextEditor: ({ value, onChange, placeholder }: any) => ( +