Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions code/frontend/src/tests/ApplyJobButton.tdd.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// src/tests/ApplyJobButton.tdd.test.tsx
// Purpose: Verify ApplyJobButton’s UI states, user interactions, and API/localStorage behavior
// 80% AI-generated, 20% human refined

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ApplyJobButton from '../components/jobsList/ApplyJobButton';
import { applyJob } from '../api/savedAndApplied/savedAndApplied';
import type { Job } from '../types/job';

// Mock the applyJob API
jest.mock('../api/savedAndApplied/savedAndApplied', () => ({
applyJob: jest.fn(),
}));

// Mock localStorage helpers without importing (avoids TS unused import error)
jest.mock('../components/jobsList/localStorageHelpers', () => ({
isJobApplied: jest.fn(() => false),
toggleAppliedJobId: jest.fn(() => true),
}));

describe('ApplyJobButton', () => {
// Mock window.open to avoid jsdom "Not implemented" error
beforeAll(() => {
window.open = jest.fn();
});

const mockJob: Job = {
_id: '1',
title: 'Frontend Developer',
url: 'https://example.com/apply',
createdAt: '',
department: 'Engineering',
descriptionBreakdown: {
employmentType: 'Full-time',
keywords: [],
oneSentenceJobSummary: 'Develop front-end apps',
salaryRangeMaxYearly: 120000,
salaryRangeMinYearly: 80000,
skillRequirements: ['React', 'TypeScript'],
workModel: 'Remote',
},
locationAddress: '123 Main St',
locationCoordinates: { lat: 0, lon: 0 },
owner: {
_id: 'o1',
companyName: 'Tech Co',
benefits: { title: 'Benefits', benefits: [] },
evaluatedSize: null,
funding: 'Series A',
isClaimed: true,
locationAddress: '123 Main St',
photo: '',
rating: '5',
slug: 'tech-co',
teamSize: 50,
},
seniority: 'Mid',
skills_suggest: ['React', 'TypeScript'],
type: 'Job',
updatedAt: '',
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders Apply button initially', () => {
render(<ApplyJobButton job={mockJob} detailed={true} />);
expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument();
});

it('handles successful apply flow', async () => {
(applyJob as jest.Mock).mockResolvedValue({ success: true });

render(<ApplyJobButton job={mockJob} detailed={true} />);
const btn = screen.getByRole('button', { name: /apply/i });
fireEvent.click(btn);

await waitFor(() => {
expect(applyJob).toHaveBeenCalledWith(mockJob);
expect(screen.getByRole('button', { name: /applied/i })).toBeInTheDocument();
expect(screen.getByText(/successfully applied/i)).toBeInTheDocument();
});
});

it('handles failed apply flow', async () => {
(applyJob as jest.Mock).mockRejectedValue(new Error('Network fail'));

render(<ApplyJobButton job={mockJob} detailed={true} />);
const btn = screen.getByRole('button', { name: /apply/i });
fireEvent.click(btn);

await waitFor(() => {
expect(applyJob).toHaveBeenCalled();
expect(screen.getByText(/network fail/i)).toBeInTheDocument();
});
});
});
97 changes: 97 additions & 0 deletions code/frontend/src/tests/SaveJobButton.tdd.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SaveJobButton.test.tsx
// Purpose: Verify SaveJobButton’s UI states and API/localStorage interactions
// 80% AI-generated, 20% human refined

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import SaveJobButton from '../components/jobsList/SaveJobButton';
import { saveJob } from '../api/savedAndApplied/savedAndApplied';
import {
isJobSaved,
toggleSavedJobId,
} from '../components/jobsList/localStorageHelpers';

jest.mock('../api/savedAndApplied/savedAndApplied', () => ({
saveJob: jest.fn(),
}));

jest.mock('../components/jobsList/localStorageHelpers', () => ({
isJobSaved: jest.fn(),
toggleSavedJobId: jest.fn(),
}));

describe('SaveJobButton', () => {
const job = { _id: 'abc123', title: 'Frontend Engineer' } as any;

beforeEach(() => {
jest.clearAllMocks();
});

it('renders Save Job initially when not saved', () => {
(isJobSaved as jest.Mock).mockReturnValue(false);
render(<SaveJobButton job={job} detailed={true} />);
expect(screen.getByRole('button', { name: /save job/i })).toBeInTheDocument();
});

it('renders Saved when job is already saved', () => {
(isJobSaved as jest.Mock).mockReturnValue(true);
render(<SaveJobButton job={job} detailed={true} />);
expect(screen.getByRole('button', { name: /saved/i })).toBeInTheDocument();
});

it('disables button when detailed=false', () => {
(isJobSaved as jest.Mock).mockReturnValue(false);
render(<SaveJobButton job={job} detailed={false} />);
const btn = screen.getByRole('button');
expect(btn).toBeDisabled();
expect(btn).toHaveAttribute('title', 'Open the detailed view to save this job');
});

it('handles successful save flow', async () => {
(isJobSaved as jest.Mock).mockReturnValue(false);
(saveJob as jest.Mock).mockResolvedValue({ success: true });
(toggleSavedJobId as jest.Mock).mockReturnValue(true);

render(<SaveJobButton job={job} detailed={true} />);

const btn = screen.getByRole('button', { name: /save job/i });
fireEvent.click(btn);

// Should change to Saving…
expect(await screen.findByText(/saving/i)).toBeInTheDocument();

await waitFor(() => {
expect(saveJob).toHaveBeenCalledWith(job);
expect(toggleSavedJobId).toHaveBeenCalledWith('abc123');
expect(screen.getByText(/saved/i)).toBeInTheDocument();
});
});

it('handles failed save flow gracefully', async () => {
(isJobSaved as jest.Mock).mockReturnValue(false);
(saveJob as jest.Mock).mockRejectedValue(new Error('Network fail'));

render(<SaveJobButton job={job} detailed={true} />);

const btn = screen.getByRole('button', { name: /save job/i });
fireEvent.click(btn);

await waitFor(() => {
expect(saveJob).toHaveBeenCalled();
expect(screen.getByText(/network fail/i)).toBeInTheDocument();
});
});

it('unsaves when already saved and clicked again', async () => {
(isJobSaved as jest.Mock).mockReturnValue(true);
(toggleSavedJobId as jest.Mock).mockReturnValue(false);

render(<SaveJobButton job={job} detailed={true} />);
const btn = screen.getByRole('button', { name: /saved/i });
fireEvent.click(btn);

await waitFor(() => {
expect(toggleSavedJobId).toHaveBeenCalledWith('abc123');
expect(screen.getByRole('button', { name: /save job/i })).toBeInTheDocument();
});
});
});
111 changes: 111 additions & 0 deletions code/frontend/src/tests/SavedJobApplyButton.tdd.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// src/tests/SavedJobApplyButton.tdd.test.tsx
// Purpose: Verify SavedJobApplyButton’s UI states, application flow, and API interactions
// 80% AI-generated, 20% human refined

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import SavedJobApplyButton from '../components/jobsList/SavedJobApplyButton';
import { applyJob } from '../api/savedAndApplied/savedAndApplied';
import { deleteSavedJob } from '../api/pages/myJobs';
import type { SavedAppliedJob } from '../api/pages/myJobs';

jest.mock('../api/savedAndApplied/savedAndApplied', () => ({
applyJob: jest.fn(),
}));

jest.mock('../api/pages/myJobs', () => ({
deleteSavedJob: jest.fn(),
}));

describe('SavedJobApplyButton', () => {
const mockJob: SavedAppliedJob = {
id: 1,
title: 'Frontend Developer',
company: 'Tech Co',
description: 'Build awesome UI',
department: 'Engineering',
url: 'http://example.com/job/frontend',
location: '123 Tech St',
locationAddress: '123 Tech St',
locationCoordinates: { lat: 0, lon: 0 },
salaryMin: 90000,
salaryMax: 120000,
employmentType: 'Full-time',
requirements: 'React,TypeScript',
type: 'Software',
seniority: 'Mid-level',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
benefits: 'Gym,Insurance',
// 修正类型
postedBy: 123,
applicationDeadline: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7天后 timestamp
};

beforeAll(() => {
// mock window.open,避免 jsdom 报错
window.open = jest.fn();
});

beforeEach(() => {
jest.clearAllMocks();
});

it('renders Apply Now button initially', () => {
render(<SavedJobApplyButton job={mockJob} detailed={true} />);
expect(screen.getByRole('button', { name: /Apply Now/i })).toBeInTheDocument();
});

it('disables button if detailed is false', () => {
render(<SavedJobApplyButton job={mockJob} detailed={false} />);
const btn = screen.getByRole('button', { name: /Apply Now/i });
expect(btn).toBeDisabled();
});

it('calls applyJob and deleteSavedJob on successful apply', async () => {
(applyJob as jest.Mock).mockResolvedValue({ success: true });
(deleteSavedJob as jest.Mock).mockResolvedValue({ success: true });
const onAppliedMock = jest.fn();

render(
<SavedJobApplyButton job={mockJob} detailed={true} onApplied={onAppliedMock} />
);
const btn = screen.getByRole('button', { name: /Apply Now/i });
fireEvent.click(btn);

await waitFor(() => {
expect(applyJob).toHaveBeenCalled();
expect(deleteSavedJob).toHaveBeenCalledWith(mockJob.id);
expect(onAppliedMock).toHaveBeenCalledWith(mockJob.id);
expect(screen.getByText(/Applied Successfully/i)).toBeInTheDocument();
});
});

it('shows error message when applyJob fails', async () => {
(applyJob as jest.Mock).mockRejectedValue(new Error('Network fail'));
render(<SavedJobApplyButton job={mockJob} detailed={true} />);
const btn = screen.getByRole('button', { name: /Apply Now/i });
fireEvent.click(btn);

await waitFor(() => {
expect(applyJob).toHaveBeenCalled();
expect(screen.getByText(/Try Again/i)).toBeInTheDocument();
expect(screen.getByText(/Network fail/i)).toBeInTheDocument();
});
});

it('opens new tab when job has URL', async () => {
(applyJob as jest.Mock).mockResolvedValue({ success: true });

render(<SavedJobApplyButton job={mockJob} detailed={true} />);
const btn = screen.getByRole('button', { name: /Apply Now/i });
fireEvent.click(btn);

await waitFor(() => {
expect(window.open).toHaveBeenCalledWith(
mockJob.url,
'_blank',
'noopener,noreferrer'
);
});
});
});
Loading