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
560 changes: 52 additions & 508 deletions src/app/scan/barcode.tsx

Large diffs are not rendered by default.

841 changes: 69 additions & 772 deletions src/app/scan/camera.tsx

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions src/components/__tests__/barcode-pending-sheets.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { fireEvent, render, screen } from '@testing-library/react-native';

import {
BarcodeContinuousBar,
BarcodePendingSheets,
} from '@/components/scan/barcode-pending-sheets';
import { makeBarcodeStyles } from '@/components/scan/barcode-styles';
import type { Pending } from '@/components/scan/use-barcode-scanning';
import { Colors } from '@/constants/theme';
import type { Magazine } from '@/types';

const styles = makeBarcodeStyles(Colors.light, { top: 0, bottom: 0 });

const magazine: Magazine = {
id: 'mag-1',
publication: 'Picsou Magazine',
issueNumber: 547,
edition: null,
language: null,
condition: null,
publicationDate: null,
barcode: null,
notes: null,
ocrText: null,
createdAt: '2026-09-01T10:00:00.000Z',
updatedAt: '2026-09-01T10:00:00.000Z',
};

function renderPending(pending: Pending) {
const callbacks = {
onResume: jest.fn(),
onConfirmAdd: jest.fn(),
onManual: jest.fn(),
};
render(
<BarcodePendingSheets
styles={styles}
pending={pending}
onResume={callbacks.onResume}
onConfirmAdd={callbacks.onConfirmAdd}
onManual={callbacks.onManual}
/>,
);
return callbacks;
}

describe('BarcodePendingSheets', () => {
it('affiche la confirmation de doublon avec le numéro et le nombre d’exemplaires', () => {
const callbacks = renderPending({
kind: 'confirm',
magazine,
ownedCount: 3,
});

expect(screen.getByText('Vous possédez déjà ce magazine')).toBeTruthy();
expect(screen.getByText('Picsou Magazine n° 547')).toBeTruthy();
expect(screen.getByText(/Exemplaires actuels : 3/)).toBeTruthy();

fireEvent.press(screen.getByTestId('pending-confirm-add'));
expect(callbacks.onConfirmAdd).toHaveBeenCalled();

fireEvent.press(screen.getByTestId('pending-confirm-cancel'));
expect(callbacks.onResume).toHaveBeenCalled();
});

it('affiche le magazine sans numéro quand il est absent', () => {
renderPending({ kind: 'confirm', magazine: { ...magazine, issueNumber: null }, ownedCount: 1 });

expect(screen.getByText('Picsou Magazine')).toBeTruthy();
});

it('affiche l’écran de succès puis reprend le scan', () => {
const callbacks = renderPending({
kind: 'success',
publication: 'Picsou Magazine',
issueNumber: 547,
});

expect(screen.getByText('Ajouté à la collection')).toBeTruthy();
expect(screen.getByText('Picsou Magazine n° 547')).toBeTruthy();

fireEvent.press(screen.getByTestId('pending-success-ok'));
expect(callbacks.onResume).toHaveBeenCalled();
});

it('affiche l’écran de succès sans numéro si l’édition n’en a pas', () => {
renderPending({ kind: 'success', publication: 'Picsou Magazine', issueNumber: null });

expect(screen.getByText('Picsou Magazine')).toBeTruthy();
});

it('suggère la saisie manuelle pour un code-barres inconnu', () => {
const callbacks = renderPending({ kind: 'unknown', barcode: '9791234567890' });

expect(screen.getByText('Code-barres inconnu')).toBeTruthy();
expect(screen.getByText('9791234567890')).toBeTruthy();

fireEvent.press(screen.getByTestId('pending-unknown-manual'));
expect(callbacks.onManual).toHaveBeenCalledWith('9791234567890');

fireEvent.press(screen.getByTestId('pending-unknown-continue'));
expect(callbacks.onResume).toHaveBeenCalled();
});

it('ne rend rien pour un état inconnu', () => {
renderPending('idle' as unknown as Pending);

expect(screen.queryByTestId('pending-confirm')).toBeNull();
expect(screen.queryByTestId('pending-success')).toBeNull();
expect(screen.queryByTestId('pending-unknown')).toBeNull();
});
});

describe('BarcodeContinuousBar', () => {
it('affiche le bandeau continu et permet de l’arrêter', () => {
const onStop = jest.fn();
render(<BarcodeContinuousBar styles={styles} onStop={onStop} />);

expect(screen.getByText(/Scan en continu/)).toBeTruthy();

fireEvent.press(screen.getByTestId('continuous-stop'));
expect(onStop).toHaveBeenCalled();
});
});
51 changes: 51 additions & 0 deletions src/components/__tests__/ocr-analysis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
buildManualParams,
confidenceLabel,
hasAnyDetected,
ANALYSIS_INTERVAL_MS,
OCR_STABLE_READS,
} from '@/components/scan/ocr-analysis';

describe('ocr-analysis (logique pure extraite de camera.tsx)', () => {
describe('buildManualParams', () => {
it('remplit publication, numéro et année', () => {
expect(
buildManualParams({ publication: 'Picsou Magazine', issueNumber: 547, date: '2023' }),
).toEqual({ publication: 'Picsou Magazine', issueNumber: '547', year: '2023' });
});

it('ignore les champs absents', () => {
expect(buildManualParams({})).toEqual({});
expect(buildManualParams({ publication: null })).toEqual({});
});

it('n’ajoute pas issueNumber à 0 ou null', () => {
expect(buildManualParams({ issueNumber: 0 })).toEqual({ issueNumber: '0' });
expect(buildManualParams({ issueNumber: null })).toEqual({});
});
});

describe('confidenceLabel', () => {
it('qualifie les seuils de confiance', () => {
expect(confidenceLabel(0.9)).toBe('élevée');
expect(confidenceLabel(0.8)).toBe('élevée');
expect(confidenceLabel(0.7)).toBe('moyenne');
expect(confidenceLabel(0.5)).toBe('moyenne');
expect(confidenceLabel(0.4)).toBe('faible');
});
});

describe('hasAnyDetected', () => {
it('détecte la présence d’au moins un champ', () => {
expect(hasAnyDetected({ publication: 'Picsou', issueNumber: null, date: null })).toBe(true);
expect(hasAnyDetected({ publication: null, issueNumber: 5, date: null })).toBe(true);
expect(hasAnyDetected({ publication: null, issueNumber: null, date: '2023' })).toBe(true);
expect(hasAnyDetected({ publication: null, issueNumber: null, date: null })).toBe(false);
});
});

it('expose les constantes d’analyse OCR', () => {
expect(ANALYSIS_INTERVAL_MS).toBe(500);
expect(OCR_STABLE_READS).toBe(2);
});
});
145 changes: 145 additions & 0 deletions src/components/__tests__/ocr-analyzing-overlay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { fireEvent, render, screen } from '@testing-library/react-native';

import { OcrAnalyzingOverlay } from '@/components/scan/ocr-analyzing-overlay';
import { makeCameraOcrStyles } from '@/components/scan/camera-ocr-styles';
import type { DetectedInfo } from '@/components/scan/ocr-analysis';
import { Colors } from '@/constants/theme';

const styles = makeCameraOcrStyles(Colors.light, { top: 0, bottom: 0 });

function renderOverlay(overrides: Partial<{ detected: DetectedInfo; weakCycles: number }> = {}) {
const callbacks = {
onOpenConfirm: jest.fn(),
onGoBarcode: jest.fn(),
onGoManual: jest.fn(),
onBack: jest.fn(),
onToggleTorch: jest.fn(),
};
render(
<OcrAnalyzingOverlay
styles={styles}
detected={overrides.detected ?? { publication: null, issueNumber: null, date: null }}
hint="Centre le magazine"
weakCycles={overrides.weakCycles ?? 0}
torchOn={false}
onOpenConfirm={callbacks.onOpenConfirm}
onGoBarcode={callbacks.onGoBarcode}
onGoManual={callbacks.onGoManual}
onBack={callbacks.onBack}
onToggleTorch={callbacks.onToggleTorch}
/>,
);
return callbacks;
}

describe('OcrAnalyzingOverlay', () => {
it('affiche le texte-guide et la lecture en cours', () => {
renderOverlay();

expect(screen.getByTestId('ocr-hint')).toHaveTextContent('Centre le magazine');
expect(screen.getByText('Lecture…')).toBeTruthy();
});

it('affiche des champs détectés avec leurs valeurs', () => {
renderOverlay({
detected: { publication: 'Picsou Magazine', issueNumber: 547, date: '2024-06' },
});

expect(screen.getByTestId('ocr-field-publication')).toHaveTextContent('Picsou Magazine');
expect(screen.getByTestId('ocr-field-issue')).toHaveTextContent('547');
expect(screen.getByTestId('ocr-field-date')).toHaveTextContent('2024-06');
});

it('affiche des points de suspension tant qu’un champ est vide', () => {
renderOverlay();

expect(screen.getByTestId('ocr-field-publication')).toHaveTextContent('…');
expect(screen.getByTestId('ocr-field-issue')).toHaveTextContent('…');
expect(screen.getByTestId('ocr-field-date')).toHaveTextContent('…');
});

it('masque la validation tant qu’aucune information n’est détectée', () => {
renderOverlay();

expect(screen.queryByTestId('ocr-confirm-detected')).toBeNull();
});

it('valide les informations détectées', () => {
const callbacks = renderOverlay({
detected: { publication: 'Picsou Magazine', issueNumber: null, date: null },
});

fireEvent.press(screen.getByTestId('ocr-confirm-detected'));
expect(callbacks.onOpenConfirm).toHaveBeenCalled();
});

it('propose code-barres, retour et torche', () => {
const callbacks = renderOverlay();

fireEvent.press(screen.getByTestId('ocr-barcode'));
expect(callbacks.onGoBarcode).toHaveBeenCalled();

fireEvent.press(screen.getByTestId('ocr-back'));
expect(callbacks.onBack).toHaveBeenCalled();

fireEvent.press(screen.getByTestId('ocr-torch'));
expect(callbacks.onToggleTorch).toHaveBeenCalled();
expect(screen.getByLabelText('Activer la torche')).toBeTruthy();
});

it('expose le libellé de torche allumée', () => {
render(
<OcrAnalyzingOverlay
styles={styles}
detected={{ publication: null, issueNumber: null, date: null }}
hint=""
weakCycles={0}
torchOn
onOpenConfirm={jest.fn()}
onGoBarcode={jest.fn()}
onGoManual={jest.fn()}
onBack={jest.fn()}
onToggleTorch={jest.fn()}
/>,
);

expect(screen.getByLabelText('Désactiver la torche')).toBeTruthy();
});

it('affiche le guide seulement après plusieurs cycles sans numéro', () => {
const callbacks = renderOverlay({
detected: { publication: 'Picsou Magazine', issueNumber: null, date: null },
weakCycles: 4,
});

expect(screen.getByTestId('ocr-guidance-card')).toBeTruthy();

fireEvent.press(screen.getByTestId('ocr-guidance-barcode'));
expect(callbacks.onGoBarcode).toHaveBeenCalled();

fireEvent.press(screen.getByTestId('ocr-guidance-manual'));
expect(callbacks.onGoManual).toHaveBeenCalledWith({
publication: 'Picsou Magazine',
issueNumber: null,
date: null,
});
});

it('n’affiche pas le guide si le numéro est détecté', () => {
renderOverlay({
detected: { publication: 'Picsou Magazine', issueNumber: 547, date: null },
weakCycles: 4,
});

expect(screen.queryByTestId('ocr-guidance-card')).toBeNull();
});

it('n’affiche pas le guide si la lecture est trop jeune', () => {
renderOverlay({
detected: { publication: 'Picsou Magazine', issueNumber: null, date: null },
weakCycles: 2,
});

expect(screen.queryByTestId('ocr-guidance-card')).toBeNull();
});
});
Loading
Loading