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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 6 additions & 39 deletions src/pages/Animals/AnimalDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ import { useAppStore } from '../../state/store';
import { Sex, Status, Breeding } from '../../models/types';
import { calculateAgeText, formatDate } from '../../utils/dates';
import { getAnimalActiveTreatments, getAnimalWeights, getAnimalTreatments, getFemaleBreedings, getAnimalById } from '../../state/selectors';
import { BreedingModal, WeightChart, GenealogyTree, QRCodeDisplay, PrintableRabbitSheet, MortalityModal, QuickWeightModal, QuickTreatmentModal } from '../../components/LazyComponents';
import { BreedingModal, WeightChart, GenealogyTree, QRCodeDisplay, MortalityModal, QuickWeightModal, QuickTreatmentModal } from '../../components/LazyComponents';
import { LitterModal } from '../../components/modals/LitterModal';
import { AdvancedGenealogyTree } from '../../components/AdvancedGenealogyTree';
import { MatingRecommendations } from '../../components/MatingRecommendations';
import { PedigreePDFService } from '../../services/pedigree-pdf.service';
import { printRabbitSheet } from '../../utils/print.utils';

interface TabPanelProps {
children?: React.ReactNode;
Expand Down Expand Up @@ -96,7 +97,6 @@ const AnimalDetailPage = () => {
const [breedingModalOpen, setBreedingModalOpen] = useState(false);
const [litterModalOpen, setLitterModalOpen] = useState(false);
const [mortalityModalOpen, setMortalityModalOpen] = useState(false);
const [printDialogOpen, setPrintDialogOpen] = useState(false);
const [selectedBreeding, setSelectedBreeding] = useState<Breeding | null>(null);
const [breedingToDelete, setBreedingToDelete] = useState<Breeding | null>(null);
const [breedingMenuAnchor, setBreedingMenuAnchor] = useState<null | HTMLElement>(null);
Expand Down Expand Up @@ -174,16 +174,10 @@ const AnimalDetailPage = () => {
};

const handlePrint = () => {
setPrintDialogOpen(true);
};

const handlePrintConfirm = () => {
// Don't close the dialog immediately, let the print happen first
setTimeout(() => {
window.print();
// Close dialog after printing
setPrintDialogOpen(false);
}, 100);
// Use the new dedicated print function instead of the dialog
if (animal) {
printRabbitSheet(animal);
}
};

const handleExportPedigree = async (animal: any) => {
Expand Down Expand Up @@ -749,33 +743,6 @@ const AnimalDetailPage = () => {
</DialogActions>
</Dialog>

{/* Print Dialog */}
<Dialog
open={printDialogOpen}
onClose={() => setPrintDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
<Box display="flex" alignItems="center" gap={1}>
<PrintIcon />
Aperçu de la fiche à imprimer
</Box>
</DialogTitle>
<DialogContent>
<PrintableRabbitSheet animal={animal} />
</DialogContent>
<DialogActions>
<Button onClick={() => setPrintDialogOpen(false)}>
Annuler
</Button>
<Button onClick={handlePrintConfirm} variant="contained" startIcon={<PrintIcon />}>
Imprimer
</Button>
</DialogActions>
</Dialog>


</Container>
);
};
Expand Down
180 changes: 180 additions & 0 deletions src/test/print.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { printRabbitSheet } from '../utils/print.utils';
import { Animal, Sex, Status } from '../models/types';

// Mock window.open
const mockWindow = {
document: {
write: vi.fn(),
close: vi.fn(),
},
onload: null as (() => void) | null,
focus: vi.fn(),
print: vi.fn(),
close: vi.fn(),
onafterprint: null as (() => void) | null,
};

describe('Print Utils', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock window.open to return our mock window
vi.stubGlobal('window', {
...window,
open: vi.fn().mockReturnValue(mockWindow),
});
});

it('should open a print window and generate correct HTML content', () => {
const testAnimal: Animal = {
id: 'test-123',
name: 'Test Rabbit',
identifier: 'TR001',
sex: Sex.Female,
status: Status.Grow,
breed: 'Rex',
birthDate: '2024-01-15T00:00:00.000Z',
origin: 'PURCHASED',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
fatherId: undefined,
motherId: undefined,
cage: undefined,
notes: undefined,
tags: [],
};

printRabbitSheet(testAnimal);

// Verify window.open was called
expect(window.open).toHaveBeenCalledWith('', '_blank', 'width=800,height=600');

// Verify document.write was called
expect(mockWindow.document.write).toHaveBeenCalledTimes(1);

// Get the HTML content that was written
const htmlContent = mockWindow.document.write.mock.calls[0][0];

// Verify essential content is present
expect(htmlContent).toContain('FICHE LAPIN');
expect(htmlContent).toContain('Test Rabbit');
expect(htmlContent).toContain('ID: TR001');
expect(htmlContent).toContain('Femelle ♀');
expect(htmlContent).toContain('15/01/2024');
expect(htmlContent).toContain('Rex');
expect(htmlContent).toContain('Code QR');
expect(htmlContent).toContain('Garenne');

// Verify CSS includes A6 page size
expect(htmlContent).toContain('@page {');
expect(htmlContent).toContain('size: A6;');
expect(htmlContent).toContain('margin: 0.5cm;');

// Verify document.close was called
expect(mockWindow.document.close).toHaveBeenCalledTimes(1);
});

it('should handle missing animal data gracefully', () => {
const testAnimal: Animal = {
id: 'test-456',
name: '',
identifier: '',
sex: Sex.Unknown,
status: Status.Grow,
breed: '',
birthDate: '',
origin: 'PURCHASED',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
fatherId: undefined,
motherId: undefined,
cage: undefined,
notes: undefined,
tags: [],
};

printRabbitSheet(testAnimal);

const htmlContent = mockWindow.document.write.mock.calls[0][0];

// Should handle empty data gracefully
expect(htmlContent).toContain('Sans nom');
expect(htmlContent).toContain('Inconnu'); // For unknown sex
expect(htmlContent).toContain('Non renseignée'); // For missing birth date
});

it('should setup print handlers correctly', () => {
const testAnimal: Animal = {
id: 'test-789',
name: 'Print Test',
identifier: 'PT001',
sex: Sex.Male,
status: Status.Reproducer,
breed: 'Angora',
birthDate: '2023-06-10T00:00:00.000Z',
origin: 'BORN_HERE',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
fatherId: undefined,
motherId: undefined,
cage: undefined,
notes: undefined,
tags: [],
};

printRabbitSheet(testAnimal);

// Simulate onload event
if (mockWindow.onload) {
mockWindow.onload();
}

// Verify print-related functions were called
expect(mockWindow.focus).toHaveBeenCalledTimes(1);
expect(mockWindow.print).toHaveBeenCalledTimes(1);

// Simulate onafterprint event
if (mockWindow.onafterprint) {
mockWindow.onafterprint();
}

// Verify window close was called after print
expect(mockWindow.close).toHaveBeenCalledTimes(1);
});

it('should handle window.open failure gracefully', () => {
// Mock window.open to return null (blocked by popup blocker)
vi.stubGlobal('window', {
...window,
open: vi.fn().mockReturnValue(null),
});

const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

const testAnimal: Animal = {
id: 'test-blocked',
name: 'Blocked Print',
identifier: 'BP001',
sex: Sex.Female,
status: Status.Grow,
breed: 'Dutch',
birthDate: '2024-03-20T00:00:00.000Z',
origin: 'PURCHASED',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
fatherId: undefined,
motherId: undefined,
cage: undefined,
notes: undefined,
tags: [],
};

// Should not throw an error
expect(() => printRabbitSheet(testAnimal)).not.toThrow();

// Should log an error message
expect(consoleErrorSpy).toHaveBeenCalledWith('Could not open print window. Please check popup blockers.');

consoleErrorSpy.mockRestore();
});
});
Loading