splitting unit + integration testing apart attempt #1
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m40s

This commit is contained in:
2025-11-30 18:08:30 -08:00
parent bcad064a1a
commit e9f555a404
6 changed files with 78 additions and 61 deletions

View File

@@ -33,6 +33,14 @@ vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () => <div data
vi.mock('./pages/admin/components/AnonymousUserBanner', () => ({ AnonymousUserBanner: () => <div data-testid="anonymous-user-banner-mock">Anonymous User Banner</div> })); vi.mock('./pages/admin/components/AnonymousUserBanner', () => ({ AnonymousUserBanner: () => <div data-testid="anonymous-user-banner-mock">Anonymous User Banner</div> }));
vi.mock('./pages/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> })); vi.mock('./pages/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> }));
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: () => <div data-testid="whats-new-modal-mock">What's New Modal</div> })); vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: () => <div data-testid="whats-new-modal-mock">What's New Modal</div> }));
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
// This must be done in any test file that imports App.tsx.
vi.mock('pdfjs-dist', () => ({
GlobalWorkerOptions: { workerSrc: '' },
getDocument: vi.fn(() => ({
promise: Promise.resolve({ getPage: vi.fn() }),
})),
}));
// By casting the apiClient to `Mocked<typeof apiClient>`, we get type-safe access // By casting the apiClient to `Mocked<typeof apiClient>`, we get type-safe access
// to Vitest's mock functions like `mockResolvedValue`. // to Vitest's mock functions like `mockResolvedValue`.

View File

@@ -63,7 +63,7 @@ describe('FlyerCorrectionTool', () => {
it('should call onClose when the close button is clicked', () => { it('should call onClose when the close button is clicked', () => {
render(<FlyerCorrectionTool {...defaultProps} />); render(<FlyerCorrectionTool {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /close/i })); fireEvent.click(screen.getByLabelText('Close correction tool'));
expect(defaultProps.onClose).toHaveBeenCalledTimes(1); expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
}); });

View File

@@ -56,7 +56,7 @@ vi.mock('./passport', () => ({
const app = express(); const app = express();
app.use(express.json({ strict: false })); app.use(express.json({ strict: false }));
app.use(cookieParser()); // Add cookie-parser middleware to populate req.cookies app.use(cookieParser()); // Add cookie-parser middleware to populate req.cookies
app.use('/api/auth', authRouter); app.use('/api/auth', authRouter); // This was missing
describe('Auth Routes (/api/auth)', () => { describe('Auth Routes (/api/auth)', () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -1,17 +1,17 @@
// src/routes/system.test.ts // src/routes/system.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import supertest from 'supertest'; import supertest from 'supertest';
import express from 'express'; import express from 'express';
import type { ExecException } from 'child_process'; import type { ExecException, ChildProcess } from 'child_process';
import systemRouter from './system'; import systemRouter from './system';
// Define a mock function at the module level. This will be our controlled version of `exec`.
const mockedExec = vi.fn();
// Mock the child_process module. We explicitly define its exports. // Mock the child_process module. We explicitly define its exports.
vi.mock('child_process', () => ({ // The mock factory MUST NOT reference variables from outside its scope due to hoisting.
exec: mockedExec, // Instead, we import the mocked function after the mock is declared.
})); vi.mock('child_process', () => ({ exec: vi.fn() }));
// Now we can safely import the mocked function and control it.
import { exec } from 'child_process';
vi.mock('../services/logger.server', () => ({ vi.mock('../services/logger.server', () => ({
logger: { logger: {
@@ -29,6 +29,7 @@ app.use('/api/system', systemRouter);
describe('System Routes (/api/system)', () => { describe('System Routes (/api/system)', () => {
beforeEach(() => { beforeEach(() => {
// We cast here to get type-safe access to mock functions like .mockImplementation
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -42,12 +43,11 @@ describe('System Routes (/api/system)', () => {
└───────────┴───────────┘ └───────────┴───────────┘
`; `;
// The `exec` callback receives (error, stdout, stderr). For success, error is null. // The `exec` callback receives (error, stdout, stderr). For success, error is null.
vi.mocked(mockedExec).mockImplementation(( // We must match the overloaded signature of `exec`. The second argument can be options or the callback.
command: string, vi.mocked(exec).mockImplementation((command, options, callback) => {
callback: (error: ExecException | null, stdout: string, stderr: string) => void const cb = typeof options === 'function' ? options : callback;
) => { cb?.(null, pm2OnlineOutput, '');
callback(null, pm2OnlineOutput, ''); return {} as ChildProcess; // Return a dummy child process object
return {}; // Return a dummy child process object
}); });
// Act // Act
@@ -63,12 +63,10 @@ describe('System Routes (/api/system)', () => {
const pm2StoppedOutput = ` const pm2StoppedOutput = `
│ status │ stopped │ │ status │ stopped │
`; `;
vi.mocked(mockedExec).mockImplementation(( vi.mocked(exec).mockImplementation((command, options, callback) => {
command: string, const cb = typeof options === 'function' ? options : callback;
callback: (error: ExecException | null, stdout: string, stderr: string) => void cb?.(null, pm2StoppedOutput, '');
) => { return {} as ChildProcess;
callback(null, pm2StoppedOutput, '');
return {};
}); });
// Act // Act
@@ -82,12 +80,10 @@ describe('System Routes (/api/system)', () => {
it('should return success: false when pm2 process does not exist', async () => { it('should return success: false when pm2 process does not exist', async () => {
// Arrange: Simulate the error and stdout when a process is not found. // Arrange: Simulate the error and stdout when a process is not found.
const processNotFoundOutput = "[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist"; const processNotFoundOutput = "[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist";
vi.mocked(mockedExec).mockImplementation(( vi.mocked(exec).mockImplementation((command, options, callback) => {
command: string, const cb = typeof options === 'function' ? options : callback;
callback: (error: ExecException | null, stdout: string, stderr: string) => void cb?.(new Error('Command failed') as ExecException, processNotFoundOutput, '');
) => { return {} as ChildProcess;
callback(new Error('Command failed') as ExecException, processNotFoundOutput, '');
return {};
}); });
// Act // Act
@@ -100,12 +96,10 @@ describe('System Routes (/api/system)', () => {
it('should return 500 on a generic exec error', async () => { it('should return 500 on a generic exec error', async () => {
// Arrange: Simulate a generic failure of the `exec` command. // Arrange: Simulate a generic failure of the `exec` command.
vi.mocked(mockedExec).mockImplementation(( vi.mocked(exec).mockImplementation((command, options, callback) => {
command: string, const cb = typeof options === 'function' ? options : callback;
callback: (error: ExecException | null, stdout: string, stderr: string) => void cb?.(new Error('Generic exec error') as ExecException, '', 'Some stderr output');
) => { return {} as ChildProcess;
callback(new Error('Generic exec error') as ExecException, '', 'Some stderr output');
return {};
}); });
// Act // Act
@@ -120,12 +114,10 @@ describe('System Routes (/api/system)', () => {
// Arrange: Simulate a scenario where the command writes to stderr but doesn't // Arrange: Simulate a scenario where the command writes to stderr but doesn't
// produce a formal error object for the callback's first argument. // produce a formal error object for the callback's first argument.
const stderrMessage = 'A non-fatal warning or configuration issue.'; const stderrMessage = 'A non-fatal warning or configuration issue.';
vi.mocked(mockedExec).mockImplementation(( vi.mocked(exec).mockImplementation((command, options, callback) => {
command: string, const cb = typeof options === 'function' ? options : callback;
callback: (error: ExecException | null, stdout: string, stderr: string) => void cb?.(null, '', stderrMessage);
) => { return {} as ChildProcess;
callback(null, '', stderrMessage);
return {};
}); });
// Act // Act

View File

@@ -47,7 +47,7 @@ app.use(express.json());
app.use('/api/users', express.json({ strict: false }), userRouter); app.use('/api/users', express.json({ strict: false }), userRouter);
describe.only('User Routes (/api/users)', () => { describe('User Routes (/api/users)', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// Default to unauthorized for all tests unless overridden in a `describe` block. // Default to unauthorized for all tests unless overridden in a `describe` block.

View File

@@ -56,29 +56,11 @@ afterEach(cleanup);
* This is the central point for mocking the database connection pool for unit tests. * This is the central point for mocking the database connection pool for unit tests.
*/ */
vi.mock('pg', () => { vi.mock('pg', () => {
// --- DIAGNOSTIC LOGGING #1: "Is the Mock Even Loading?" ---
// This log will appear once at the start of the test run to prove that this global mock is being used.
console.log('[vitest-mock-pg] Global `pg` mock from unit-setup.ts is being initialized.');
// These are the mock functions that will replace the real 'pg' methods.
const mockQuery = vi.fn(); const mockQuery = vi.fn();
const mockRelease = vi.fn(); const mockRelease = vi.fn();
const mockConnect = vi.fn().mockResolvedValue({ const mockConnect = vi.fn().mockResolvedValue({ query: mockQuery, release: mockRelease });
query: mockQuery, const mockPool = vi.fn(() => ({ query: mockQuery, connect: mockConnect, totalCount: 10, idleCount: 5, waitingCount: 0, end: vi.fn() }));
release: mockRelease,
});
// Mock the Pool class constructor.
const mockPool = vi.fn(() => ({
query: mockQuery,
connect: mockConnect,
totalCount: 10,
idleCount: 5,
waitingCount: 0,
end: vi.fn(),
}));
// The object returned here becomes the exports of the 'pg' module for all unit tests.
return { return {
__esModule: true, // This is important for ES module interoperability. __esModule: true, // This is important for ES module interoperability.
Pool: mockPool, Pool: mockPool,
@@ -88,6 +70,27 @@ vi.mock('pg', () => {
}; };
}); });
/**
* Mocks the Google Generative AI package. This prevents real API calls during tests.
*/
vi.mock('@google/generative-ai', () => {
const mockGenerativeModel = {
generateContent: vi.fn().mockResolvedValue({
response: {
text: () => 'Mocked AI response',
candidates: [{ content: { parts: [{ text: 'Mocked AI response' }] } }],
},
}),
};
return {
GoogleGenerativeAI: vi.fn(() => ({
getGenerativeModel: () => mockGenerativeModel,
})),
HarmCategory: {},
HarmBlockThreshold: {},
};
});
/** /**
* Mocks the entire apiClient module. * Mocks the entire apiClient module.
* This ensures that all test files that import from apiClient will get this mocked version. * This ensures that all test files that import from apiClient will get this mocked version.
@@ -160,4 +163,18 @@ vi.mock('../../services/logger', () => ({
vi.mock('../../services/notificationService', () => ({ vi.mock('../../services/notificationService', () => ({
notifySuccess: vi.fn(), notifySuccess: vi.fn(),
notifyError: vi.fn(), notifyError: vi.fn(),
}));
/**
* Mocks the `react-hot-toast` library to prevent actual toasts from rendering
* during tests. This also provides mock controls for functions like `toast.loading`.
*/
vi.mock('react-hot-toast', () => ({
default: {
success: vi.fn(),
error: vi.fn(),
loading: vi.fn(() => 'toast-id-123'), // Return a dummy ID
dismiss: vi.fn(),
},
Toaster: () => null, // The component renders nothing in tests
})); }));