ContactHeuristicParserSpatialTest.swift 143 lignes · 5676 octets
import XCTest
@testable import Card2vcf

final class ContactHeuristicParserSpatialTest: XCTestCase {

    private func ligne(
        _ text: String,
        top: Int,
        height: Int = 30,
        left: Int = 40,
        right: Int = 500,
        confidence: Float = 90
    ) -> OcrLine {
        OcrLine(text: text, box: OcrBox(left: left, top: top, right: right, bottom: top + height), confidence: confidence)
    }

    private func ocr(
        _ lines: [OcrLine],
        phones: [String] = [],
        emails: [String] = [],
        urls: [String] = []
    ) -> OcrResult {
        OcrResult(
            rawText: lines.map(\.text).joined(separator: "\n"),
            lines: lines.map(\.text),
            phones: phones,
            emails: emails,
            urls: urls,
            spatialLines: lines
        )
    }

    func testAdresseMultiLignesAssemblee() {
        let card = ContactHeuristicParser.parse(ocr([
            ligne("Sonia MARTIN", top: 100, height: 40),
            ligne("561 allée des Noisetiers", top: 400),
            ligne("69760 Limonest - France", top: 440),
        ]))
        XCTAssertEqual("561 allée des Noisetiers, 69760 Limonest - France", card.address)
    }

    func testNomEnGrosCaracteresSansMajusculesDetecte() {
        let card = ContactHeuristicParser.parse(ocr(
            [
                ligne("sonia martin", top: 100, height: 64),
                ligne("directrice commerciale", top: 180, height: 28),
            ],
            emails: ["sonia.martin@voltea.fr"]
        ))
        XCTAssertEqual("Martin", card.lastName)
        XCTAssertEqual("sonia", card.firstName?.lowercased())
        XCTAssertEqual("directrice commerciale", card.jobTitle)
    }

    func testSocieteDominanteEnMajusculesSepareeDuNom() {
        let card = ContactHeuristicParser.parse(ocr(
            [
                ligne("VOLTEA SOLUTIONS", top: 40, height: 50),
                ligne("Sonia MARTIN", top: 200, height: 34),
                ligne("Responsable grands comptes", top: 244, height: 28),
            ],
            emails: ["sonia.martin@voltea.fr"]
        ))
        XCTAssertEqual("VOLTEA SOLUTIONS", card.company)
        XCTAssertEqual("Sonia Martin", card.fullName)
        XCTAssertEqual("Responsable grands comptes", card.jobTitle)
    }

    func testNomToutEnMajusculesResteUnNomPasUneSociete() {
        // Très courant en France : « SONIA MARTIN » tout en capitales.
        let card = ContactHeuristicParser.parse(ocr(
            [
                ligne("SONIA MARTIN", top: 100, height: 34),
                ligne("Directrice commerciale", top: 150, height: 28),
            ],
            emails: ["sonia.martin@voltea.fr"]
        ))
        XCTAssertEqual("Sonia Martin", card.fullName)
        XCTAssertEqual("Martin", card.lastName)
        XCTAssertEqual("Voltea", card.company) // depuis le domaine email, pas le nom
    }

    func testDeuxColonnesSansMelangeAdresseTelephone() {
        let card = ContactHeuristicParser.parse(ocr(
            [
                ligne("Sonia MARTIN", top: 60, height: 40, left: 40, right: 900),
                ligne("12 rue des Fleurs", top: 400, left: 40, right: 380),
                ligne("75011 Paris", top: 440, left: 40, right: 380),
                ligne("Tél. 04 75 35 12 34", top: 405, left: 600, right: 980),
                ligne("sonia@voltea.fr", top: 445, left: 600, right: 980),
            ],
            phones: ["0475351234"],
            emails: ["sonia@voltea.fr"]
        ))
        XCTAssertEqual("12 rue des Fleurs, 75011 Paris", card.address)
        XCTAssertEqual(["0475351234"], card.phones)
    }

    func testLibelleTelephoneAvecValeurSurLaLigneSuivante() {
        let card = ContactHeuristicParser.parse(ocr([
            ligne("Sonia MARTIN", top: 60, height: 40),
            ligne("Tél. :", top: 300),
            ligne("04 75 35 12 34", top: 340),
        ]))
        XCTAssertTrue(card.phones.contains("0475351234"), "téléphone attendu: \(card.phones)")
    }

    func testChampsDouteuxMarquesSousLeSeuilDeConfiance() {
        let card = ContactHeuristicParser.parse(ocr([
            ligne("Sonia MARTIN", top: 100, height: 40, confidence: 45),
            ligne("561 allée des Noisetiers", top: 400, confidence: 92),
            ligne("69760 Limonest - France", top: 440, confidence: 91),
        ]))
        XCTAssertTrue(card.champsDouteux.contains("fullName"), "nom à 45% doit être douteux: \(card.champsDouteux)")
        XCTAssertFalse(card.champsDouteux.contains("address"), "adresse à ~91% doit être sûre: \(card.champsDouteux)")
    }

    func testFallbackSansBoxesAucunChampDouteux() {
        let card = ContactHeuristicParser.parse(
            OcrResult(rawText: "Sonia MARTIN\nDirectrice", lines: ["Sonia MARTIN", "Directrice"])
        )
        XCTAssertEqual([], card.champsDouteux)
    }

    func testCheminSpatialSeulementAvecBoxes() {
        let textes = [
            "Sonia MARTIN",
            "561 allée des Noisetiers",
            "69760 Limonest - France",
        ]
        let avecBoxes = ContactHeuristicParser.parse(ocr([
            ligne(textes[0], top: 100, height: 40),
            ligne(textes[1], top: 400),
            ligne(textes[2], top: 440),
        ]))
        let sansBoxes = ContactHeuristicParser.parse(
            OcrResult(rawText: textes.joined(separator: "\n"), lines: textes)
        )
        // Spatial : adresse complète ; legacy : une seule ligne (la plus longue).
        XCTAssertEqual("561 allée des Noisetiers, 69760 Limonest - France", avecBoxes.address)
        XCTAssertEqual("561 allée des Noisetiers", sansBoxes.address)
    }
}