feat(tests): implement unit tests for job post and application forms and fix dashboard tests
This commit is contained in:
parent
d79fa8e97a
commit
812affb803
5 changed files with 391 additions and 11 deletions
|
|
@ -12,6 +12,7 @@ const customJestConfig = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
},
|
},
|
||||||
|
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/', '<rootDir>/e2e/'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
|
|
||||||
149
frontend/src/app/jobs/[id]/apply/page.test.tsx
Normal file
149
frontend/src/app/jobs/[id]/apply/page.test.tsx
Normal file
|
|
@ -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: () => <div data-testid="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(<JobApplicationPage params={params} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Application: Software Engineer")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Tech Corp/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates step 1 fields", async () => {
|
||||||
|
render(<JobApplicationPage params={params} />);
|
||||||
|
|
||||||
|
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(<JobApplicationPage params={params} />);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
197
frontend/src/app/post-job/page.test.tsx
Normal file
197
frontend/src/app/post-job/page.test.tsx
Normal file
|
|
@ -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) => (
|
||||||
|
<textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
data-testid="rich-text-editor"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock LocationPicker component
|
||||||
|
jest.mock("@/components/location-picker", () => ({
|
||||||
|
LocationPicker: ({ value, onChange }: any) => (
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder="Location"
|
||||||
|
data-testid="location-picker"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("PostJobPage", () => {
|
||||||
|
const mockPush = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(useRouter as jest.Mock).mockReturnValue({ push: mockPush });
|
||||||
|
global.fetch = jest.fn();
|
||||||
|
// Mock scroll methods not in test env
|
||||||
|
window.scrollTo = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders step 1 initially", () => {
|
||||||
|
render(<PostJobPage />);
|
||||||
|
expect(screen.getByText("Postar uma Vaga")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Step 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText("My Company")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates company fields before proceeding", () => {
|
||||||
|
render(<PostJobPage />);
|
||||||
|
|
||||||
|
// Try next without filling
|
||||||
|
fireEvent.click(screen.getByText("Next Step"));
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Preencha os dados obrigatórios da empresa");
|
||||||
|
|
||||||
|
// Fill password mismatch
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("My Company"), { target: { value: "Test Co" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("contato@empresa.com"), { target: { value: "test@co.com" } });
|
||||||
|
|
||||||
|
const passwords = screen.getAllByPlaceholderText("••••••••");
|
||||||
|
fireEvent.change(passwords[0], { target: { value: "12345678" } });
|
||||||
|
fireEvent.change(passwords[1], { target: { value: "mismatch" } });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next Step"));
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("As senhas não coincidem");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("proceeds to step 2 when company info is valid", () => {
|
||||||
|
render(<PostJobPage />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("My Company"), { target: { value: "Test Co" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("contato@empresa.com"), { target: { value: "test@co.com" } });
|
||||||
|
|
||||||
|
const passwords = screen.getAllByPlaceholderText("••••••••");
|
||||||
|
fireEvent.change(passwords[0], { target: { value: "password123" } });
|
||||||
|
fireEvent.change(passwords[1], { target: { value: "password123" } });
|
||||||
|
|
||||||
|
// Need to fill job description as well now
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("e.g. Developer"), { target: { value: "Frontend Dev" } });
|
||||||
|
const editors = screen.getAllByTestId("rich-text-editor");
|
||||||
|
if (editors.length > 1) {
|
||||||
|
fireEvent.change(editors[1], { target: { value: "Job Desc" } });
|
||||||
|
} else {
|
||||||
|
fireEvent.change(editors[0], { target: { value: "Job Desc" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next Step"));
|
||||||
|
|
||||||
|
expect(screen.getByText("Step 2")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("submits the form successfully", async () => {
|
||||||
|
(global.fetch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ token: "fake-token" }),
|
||||||
|
}) // Register
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ id: "job-123" }),
|
||||||
|
}); // Create Job
|
||||||
|
|
||||||
|
render(<PostJobPage />);
|
||||||
|
|
||||||
|
// Step 1
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("My Company"), { target: { value: "Test Co" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("contato@empresa.com"), { target: { value: "test@co.com" } });
|
||||||
|
|
||||||
|
const passwords = screen.getAllByPlaceholderText("••••••••");
|
||||||
|
fireEvent.change(passwords[0], { target: { value: "password123" } });
|
||||||
|
fireEvent.change(passwords[1], { target: { value: "password123" } });
|
||||||
|
|
||||||
|
// Fill Job Details (Step 1)
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("e.g. Developer"), { target: { value: "Frontend Dev" } });
|
||||||
|
|
||||||
|
const editors = screen.getAllByTestId("rich-text-editor");
|
||||||
|
if (editors.length > 1) {
|
||||||
|
fireEvent.change(editors[1], { target: { value: "Job Desc" } });
|
||||||
|
} else {
|
||||||
|
fireEvent.change(editors[0], { target: { value: "Job Desc" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next Step"));
|
||||||
|
|
||||||
|
// Check we are on Step 2
|
||||||
|
expect(screen.getByText("Step 2")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
fireEvent.click(screen.getByText("Publish Job"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||||
|
expect(toast.success).toHaveBeenCalled();
|
||||||
|
expect(mockPush).toHaveBeenCalledWith("/dashboard/jobs");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -114,30 +114,52 @@ export default function PostJobPage() {
|
||||||
return value.replace(/[^\d\s-]/g, "");
|
return value.replace(/[^\d\s-]/g, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const validateForm = () => {
|
||||||
if (!company.name || !company.email || !company.password) {
|
if (!company.name || !company.email || !company.password) {
|
||||||
toast.error("Preencha os dados obrigatórios da empresa");
|
toast.error("Preencha os dados obrigatórios da empresa");
|
||||||
setStep(1);
|
setStep(1); // Ensure we are on step 1 for company data errors
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (company.password !== company.confirmPassword) {
|
if (company.password !== company.confirmPassword) {
|
||||||
toast.error("As senhas não coincidem");
|
toast.error("As senhas não coincidem");
|
||||||
setStep(1);
|
setStep(1); // Ensure we are on step 1 for password mismatch
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (company.password.length < 8) {
|
if (company.password.length < 8) {
|
||||||
toast.error("A senha deve ter pelo menos 8 caracteres");
|
toast.error("A senha deve ter pelo menos 8 caracteres");
|
||||||
setStep(1);
|
setStep(1); // Ensure we are on step 1 for password length
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!job.title || !job.description) {
|
if (!job.title || !job.description) {
|
||||||
toast.error("Preencha os dados da vaga");
|
toast.error("Preencha os dados da vaga");
|
||||||
setStep(2);
|
setStep(1); // Stay on step 1 for job data errors
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
// Only validate step 1 fields to move to step 2
|
||||||
|
if (!company.name || !company.email || !company.password) {
|
||||||
|
toast.error("Preencha os dados obrigatórios da empresa");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (company.password !== company.confirmPassword) {
|
||||||
|
toast.error("As senhas não coincidem");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (company.password.length < 8) {
|
||||||
|
toast.error("A senha deve ter pelo menos 8 caracteres");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -607,7 +629,7 @@ export default function PostJobPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={() => setStep(2)} className="w-full">
|
<Button onClick={handleNext} className="w-full">
|
||||||
{t.buttons.next}
|
{t.buttons.next}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { render, screen } from "@testing-library/react"
|
import { render, screen } from "@testing-library/react"
|
||||||
import CandidateDashboard from "./candidate-dashboard"
|
import { CandidateDashboardContent } from "./candidate-dashboard"
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
jest.mock("@/lib/auth", () => ({
|
jest.mock("@/lib/auth", () => ({
|
||||||
|
|
@ -7,6 +7,17 @@ jest.mock("@/lib/auth", () => ({
|
||||||
isAuthenticated: jest.fn().mockReturnValue(true),
|
isAuthenticated: jest.fn().mockReturnValue(true),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock("@/contexts/notification-context", () => ({
|
||||||
|
useNotifications: jest.fn().mockReturnValue({ notifications: [], markAsRead: jest.fn() }),
|
||||||
|
useNotify: jest.fn().mockReturnValue(jest.fn()),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock("@/lib/i18n", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
jest.mock("@/lib/api", () => ({
|
jest.mock("@/lib/api", () => ({
|
||||||
jobsApi: {
|
jobsApi: {
|
||||||
list: jest.fn().mockResolvedValue({ data: [], pagination: {} }),
|
list: jest.fn().mockResolvedValue({ data: [], pagination: {} }),
|
||||||
|
|
@ -40,7 +51,7 @@ jest.mock("@/components/ui/skeleton", () => ({
|
||||||
|
|
||||||
describe("CandidateDashboard", () => {
|
describe("CandidateDashboard", () => {
|
||||||
it("renders welcome message", async () => {
|
it("renders welcome message", async () => {
|
||||||
render(<CandidateDashboard />)
|
render(<CandidateDashboardContent />)
|
||||||
// Check for static text or known content
|
// Check for static text or known content
|
||||||
// Assuming dashboard has "Welcome" or similar
|
// Assuming dashboard has "Welcome" or similar
|
||||||
// Or check for section headers "My Applications", "Recommended Jobs"
|
// Or check for section headers "My Applications", "Recommended Jobs"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue