From 888028324198d0100b94519bc32fef27bc11157c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 11:59:37 +0100 Subject: [PATCH 1/7] add unit tests --- src/v2/parsing/index.ts | 2 +- .../{parsing => product}/extraction.spec.ts | 66 ++++++++++--------- tests/v2/product/utils.ts | 11 ++++ 3 files changed, 48 insertions(+), 31 deletions(-) rename tests/v2/{parsing => product}/extraction.spec.ts (90%) create mode 100644 tests/v2/product/utils.ts diff --git a/src/v2/parsing/index.ts b/src/v2/parsing/index.ts index b5260f9a..b449079d 100644 --- a/src/v2/parsing/index.ts +++ b/src/v2/parsing/index.ts @@ -16,4 +16,4 @@ export { export { LocalResponse } from "./localResponse.js"; export { BaseResponse } from "./baseResponse.js"; export type { ResponseConstructor } from "./baseResponse.js"; -export { RawText, RagMetadata } from "./inference/field/index.js"; +export * as field from "./inference/field/index.js"; diff --git a/tests/v2/parsing/extraction.spec.ts b/tests/v2/product/extraction.spec.ts similarity index 90% rename from tests/v2/parsing/extraction.spec.ts rename to tests/v2/product/extraction.spec.ts index 74c4eddc..1dd3c950 100644 --- a/tests/v2/parsing/extraction.spec.ts +++ b/tests/v2/product/extraction.spec.ts @@ -8,13 +8,10 @@ import { ObjectField, SimpleField, } from "@/v2/parsing/inference/field/index.js"; -import { - LocalResponse, - RagMetadata, - RawText, -} from "@/v2/parsing/index.js"; +import { field } from "@/v2/parsing/index.js"; import { V2_PRODUCT_PATH } from "../../index.js"; import { ExtractionResponse } from "@/v2/product/index.js"; +import { loadV2Response } from "../product/utils.js"; const findocPath = path.join(V2_PRODUCT_PATH, "extraction", "financial_document"); const extractionPath = path.join(V2_PRODUCT_PATH, "extraction"); @@ -23,16 +20,11 @@ const standardFieldPath = path.join(extractionPath, "standard_field_types.json") const standardFieldRstPath = path.join(extractionPath, "standard_field_types.rst"); const locationFieldPath = path.join(findocPath, "complete_with_coordinates.json"); -async function loadV2Extraction(resourcePath: string): Promise { - const localResponse = new LocalResponse(resourcePath); - await localResponse.init(); - return localResponse.deserializeResponse(ExtractionResponse); -} - describe("MindeeV2 - Extraction Response", async () => { describe("Financial Document", async () => { it("should load a blank inference with valid properties", async () => { - const response = await loadV2Extraction( + const response = await loadV2Response( + ExtractionResponse, path.join(findocPath, "blank.json") ); const fields = response.inference.result.fields; @@ -64,7 +56,8 @@ describe("MindeeV2 - Extraction Response", async () => { }); it("should load a complete inference with valid properties", async () => { - const response = await loadV2Extraction( + const response = await loadV2Response( + ExtractionResponse, path.join(findocPath, "complete.json") ); const inference = response.inference; @@ -129,7 +122,9 @@ describe("MindeeV2 - Extraction Response", async () => { describe("Deeply Nested", async () => { it("should load a deep nested object", async () => { - const response = await loadV2Extraction(deepNestedFieldPath); + const response = await loadV2Response( + ExtractionResponse, deepNestedFieldPath + ); const fields = response.inference.result.fields; expect(fields.get("field_simple")).to.be.an.instanceof(SimpleField); expect(fields.get("field_object")).to.be.an.instanceof(ObjectField); @@ -165,7 +160,9 @@ describe("MindeeV2 - Extraction Response", async () => { describe("Standard Field Types", async () => { it("should recognize simple fields", async () => { - const response = await loadV2Extraction(standardFieldPath); + const response = await loadV2Response( + ExtractionResponse, standardFieldPath + ); const fields = response.inference.result.fields; expect(fields.get("field_simple_string")).to.be.instanceOf(SimpleField); @@ -211,7 +208,9 @@ describe("MindeeV2 - Extraction Response", async () => { }); it("should recognize simple list fields", async () => { - const response = await loadV2Extraction(standardFieldPath); + const response = await loadV2Response( + ExtractionResponse, standardFieldPath + ); const fields = response.inference.result.fields; expect(fields.get("field_simple_list")).to.be.instanceOf(ListField); @@ -226,7 +225,9 @@ describe("MindeeV2 - Extraction Response", async () => { }); it("should recognize object fields", async () => { - const response = await loadV2Extraction(standardFieldPath); + const response = await loadV2Response( + ExtractionResponse, standardFieldPath + ); const fields = response.inference.result.fields; expect(fields.get("field_object")).to.be.instanceOf(ObjectField); @@ -246,7 +247,9 @@ describe("MindeeV2 - Extraction Response", async () => { }); it("should recognize object list fields", async () => { - const response = await loadV2Extraction(standardFieldPath); + const response = await loadV2Response( + ExtractionResponse, standardFieldPath + ); const fields = response.inference.result.fields; expect(fields.get("field_object_list")).to.be.instanceOf(ListField); @@ -272,13 +275,13 @@ describe("MindeeV2 - Extraction Response", async () => { describe("Raw Text", async () => { it("raw text should be exposed", async () => { - const response = await loadV2Extraction( - path.join(extractionPath, "raw_texts.json") + const response = await loadV2Response( + ExtractionResponse, path.join(extractionPath, "raw_texts.json") ); expect(response.inference.result.rag).to.be.undefined; const rawText = response.inference.result.rawText; - expect(rawText).to.be.instanceOf(RawText); + expect(rawText).to.be.instanceOf(field.RawText); const pages = rawText?.pages; if (pages === undefined) throw new Error("pages is undefined"); @@ -291,27 +294,29 @@ describe("MindeeV2 - Extraction Response", async () => { describe("RAG Metadata", async () => { it("RAG metadata when matched", async () => { - const response = await loadV2Extraction( - path.join(extractionPath, "rag_matched.json") + const response = await loadV2Response( + ExtractionResponse, path.join(extractionPath, "rag_matched.json") ); const rag = response.inference.result.rag; - expect(rag).to.be.instanceOf(RagMetadata); + expect(rag).to.be.instanceOf(field.RagMetadata); expect(rag?.retrievedDocumentId).to.eq("12345abc-1234-1234-1234-123456789abc"); }); it("RAG metadata when not matched", async () => { - const response = await loadV2Extraction( - path.join(extractionPath, "rag_not_matched.json") + const response = await loadV2Response( + ExtractionResponse, path.join(extractionPath, "rag_not_matched.json") ); const rag = response.inference.result.rag; - expect(rag).to.be.instanceOf(RagMetadata); + expect(rag).to.be.instanceOf(field.RagMetadata); expect(rag?.retrievedDocumentId).to.be.undefined; }); }); describe("RST Display", async () => { it("to be properly exposed", async () => { - const response = await loadV2Extraction(standardFieldPath); + const response = await loadV2Response( + ExtractionResponse, standardFieldPath + ); const rstString = await fs.readFile(standardFieldRstPath, "utf8"); expect(response.inference).to.not.be.null; @@ -321,8 +326,9 @@ describe("MindeeV2 - Extraction Response", async () => { describe("Field Locations and Confidence", async () => { it("to be properly exposed", async () => { - const response = await loadV2Extraction(locationFieldPath); - + const response = await loadV2Response( + ExtractionResponse, locationFieldPath + ); expect(response.inference).to.not.be.null; const dateField = response.inference.result.fields.get("date") as SimpleField; diff --git a/tests/v2/product/utils.ts b/tests/v2/product/utils.ts new file mode 100644 index 00000000..d72c5253 --- /dev/null +++ b/tests/v2/product/utils.ts @@ -0,0 +1,11 @@ +import { BaseResponse, ResponseConstructor } from "@/v2/parsing/index.js"; +import { LocalResponse } from "@/v2/index.js"; + +export async function loadV2Response( + responseClass: ResponseConstructor, + resourcePath: string +): Promise { + const localResponse = new LocalResponse(resourcePath); + await localResponse.init(); + return localResponse.deserializeResponse(responseClass); +} From 6a040d3f2a2a14094f21ddf23128ec03ebe36099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 12:06:52 +0100 Subject: [PATCH 2/7] :bug: fix for trying to init LocalResponse multiple times --- src/parsing/localResponseBase.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/parsing/localResponseBase.ts b/src/parsing/localResponseBase.ts index e920f495..d1f6cfa1 100644 --- a/src/parsing/localResponseBase.ts +++ b/src/parsing/localResponseBase.ts @@ -25,6 +25,9 @@ export abstract class LocalResponseBase { /** * @param inputFile - The input file, which can be a Buffer, string, or PathLike. */ + if (this.initialized) { + return; + } if (Buffer.isBuffer(this.inputHandle)) { this.file = this.inputHandle; } else if (typeof this.inputHandle === "string") { From 25cc8ee3722eb9de4011e24ceb9a7721538d69e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 12:08:56 +0100 Subject: [PATCH 3/7] add unit tests --- src/v2/product/index.ts | 8 ++++---- tests/v2/product/classification.spec.ts | 17 ++++++++++++++++ tests/v2/product/crop.spec.ts | 25 ++++++++++++++++++++++++ tests/v2/product/extraction.spec.ts | 2 +- tests/v2/product/ocr.spec.ts | 26 +++++++++++++++++++++++++ tests/v2/product/split.spec.ts | 26 +++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/v2/product/classification.spec.ts create mode 100644 tests/v2/product/crop.spec.ts create mode 100644 tests/v2/product/ocr.spec.ts create mode 100644 tests/v2/product/split.spec.ts diff --git a/src/v2/product/index.ts b/src/v2/product/index.ts index 5fc60b74..d56cccbb 100644 --- a/src/v2/product/index.ts +++ b/src/v2/product/index.ts @@ -1,5 +1,5 @@ -export { Classification } from "./classification/index.js"; -export { Crop } from "./crop/index.js"; +export { Classification, ClassificationResponse } from "./classification/index.js"; +export { Crop, CropResponse } from "./crop/index.js"; export { Extraction, ExtractionResponse } from "./extraction/index.js"; -export { Ocr } from "./ocr/index.js"; -export { Split } from "./split/index.js"; +export { Ocr, OcrResponse } from "./ocr/index.js"; +export { Split, SplitResponse } from "./split/index.js"; diff --git a/tests/v2/product/classification.spec.ts b/tests/v2/product/classification.spec.ts new file mode 100644 index 00000000..5ecc9d93 --- /dev/null +++ b/tests/v2/product/classification.spec.ts @@ -0,0 +1,17 @@ +import { expect } from "chai"; +import path from "node:path"; +import { V2_PRODUCT_PATH } from "../../index.js"; +import { ClassificationResponse } from "@/v2/product/index.js"; +import { loadV2Response } from "./utils.js"; + + +describe("MindeeV2 - Classification Response", async () => { + it("should load a single result", async () => { + const response = await loadV2Response( + ClassificationResponse, + path.join(V2_PRODUCT_PATH, "classification", "classification_single.json") + ); + const classification = response.inference.result.classification; + expect(classification.documentType).to.equal("invoice"); + }); +}); diff --git a/tests/v2/product/crop.spec.ts b/tests/v2/product/crop.spec.ts new file mode 100644 index 00000000..5f74bf9e --- /dev/null +++ b/tests/v2/product/crop.spec.ts @@ -0,0 +1,25 @@ +import { expect } from "chai"; +import path from "node:path"; +import { V2_PRODUCT_PATH } from "../../index.js"; +import { CropResponse } from "@/v2/product/index.js"; +import { loadV2Response } from "./utils.js"; + +describe("MindeeV2 - Crop Response", async () => { + it("should load a single result", async () => { + const response = await loadV2Response( + CropResponse, + path.join(V2_PRODUCT_PATH, "crop", "crop_single.json") + ); + const crops = response.inference.result.crops; + expect(crops).to.be.an("array").that.has.lengthOf(1); + }); + + it("should load multiple results", async () => { + const response = await loadV2Response( + CropResponse, + path.join(V2_PRODUCT_PATH, "crop", "crop_multiple.json") + ); + const crops = response.inference.result.crops; + expect(crops).to.be.an("array").that.has.lengthOf(2); + }); +}); diff --git a/tests/v2/product/extraction.spec.ts b/tests/v2/product/extraction.spec.ts index 1dd3c950..ac7f3855 100644 --- a/tests/v2/product/extraction.spec.ts +++ b/tests/v2/product/extraction.spec.ts @@ -11,7 +11,7 @@ import { import { field } from "@/v2/parsing/index.js"; import { V2_PRODUCT_PATH } from "../../index.js"; import { ExtractionResponse } from "@/v2/product/index.js"; -import { loadV2Response } from "../product/utils.js"; +import { loadV2Response } from "./utils.js"; const findocPath = path.join(V2_PRODUCT_PATH, "extraction", "financial_document"); const extractionPath = path.join(V2_PRODUCT_PATH, "extraction"); diff --git a/tests/v2/product/ocr.spec.ts b/tests/v2/product/ocr.spec.ts new file mode 100644 index 00000000..7cb19658 --- /dev/null +++ b/tests/v2/product/ocr.spec.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import path from "node:path"; +import { V2_PRODUCT_PATH } from "../../index.js"; +import { OcrResponse } from "@/v2/product/index.js"; +import { loadV2Response } from "./utils.js"; + + +describe("MindeeV2 - OCR Response", async () => { + it("should load a single result", async () => { + const response = await loadV2Response( + OcrResponse, + path.join(V2_PRODUCT_PATH, "ocr", "ocr_single.json") + ); + const pages = response.inference.result.pages; + expect(pages).to.be.an("array").that.has.lengthOf(1); + }); + + it("should load multiple results", async () => { + const response = await loadV2Response( + OcrResponse, + path.join(V2_PRODUCT_PATH, "ocr", "ocr_multiple.json") + ); + const pages = response.inference.result.pages; + expect(pages).to.be.an("array").that.has.lengthOf(3); + }); +}); diff --git a/tests/v2/product/split.spec.ts b/tests/v2/product/split.spec.ts new file mode 100644 index 00000000..c79f234b --- /dev/null +++ b/tests/v2/product/split.spec.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; +import path from "node:path"; +import { V2_PRODUCT_PATH } from "../../index.js"; +import { SplitResponse } from "@/v2/product/index.js"; +import { loadV2Response } from "./utils.js"; + + +describe("MindeeV2 - Split Response", async () => { + it("should load a single result", async () => { + const response = await loadV2Response( + SplitResponse, + path.join(V2_PRODUCT_PATH, "split", "split_single.json") + ); + const splits = response.inference.result.splits; + expect(splits).to.be.an("array").that.has.lengthOf(1); + }); + + it("should load multiple results", async () => { + const response = await loadV2Response( + SplitResponse, + path.join(V2_PRODUCT_PATH, "split", "split_multiple.json") + ); + const splits = response.inference.result.splits; + expect(splits).to.be.an("array").that.has.lengthOf(3); + }); +}); From 7bef028af6158e7d39af3ffe518f066dc94176ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 12:14:59 +0100 Subject: [PATCH 4/7] make local response errors easier to debug --- src/v2/parsing/localResponse.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/v2/parsing/localResponse.ts b/src/v2/parsing/localResponse.ts index 657e7b7b..165c75e8 100644 --- a/src/v2/parsing/localResponse.ts +++ b/src/v2/parsing/localResponse.ts @@ -24,8 +24,9 @@ export class LocalResponse extends LocalResponseBase { ): Promise { try { return new responseClass(await this.asDict()); - } catch { - throw new MindeeError("Invalid response provided."); + } catch (error) { + console.error(error); + throw new MindeeError(`Invalid response provided: ${error}`); } } } From 637be73f0ac58ea22e86496653a8c970e28a715e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 12:29:21 +0100 Subject: [PATCH 5/7] :bug: fix crop item --- src/v2/product/crop/cropItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/product/crop/cropItem.ts b/src/v2/product/crop/cropItem.ts index 470042b0..a36c3716 100644 --- a/src/v2/product/crop/cropItem.ts +++ b/src/v2/product/crop/cropItem.ts @@ -6,7 +6,7 @@ export class CropItem { location: FieldLocation; constructor(serverResponse: StringDict) { - this.objectType = serverResponse["objectType"]; + this.objectType = serverResponse["object_type"]; this.location = new FieldLocation(serverResponse["location"]); } From 54f27080b2c69f2ee88b760a7cf444d92fa02e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 12:51:56 +0100 Subject: [PATCH 6/7] fix polygons --- .../parsing/inference/field/fieldLocation.ts | 2 +- src/v2/product/index.ts | 2 +- src/v2/product/ocr/ocrParameters.ts | 2 +- src/v2/product/ocr/ocrWord.ts | 2 +- tests/v2/product/crop.spec.ts | 70 ++++++++++++++++++- tests/v2/product/utils.ts | 1 - 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/v2/parsing/inference/field/fieldLocation.ts b/src/v2/parsing/inference/field/fieldLocation.ts index 14b9ae33..dbb314af 100644 --- a/src/v2/parsing/inference/field/fieldLocation.ts +++ b/src/v2/parsing/inference/field/fieldLocation.ts @@ -12,7 +12,7 @@ export class FieldLocation { readonly page: number | undefined; constructor(serverResponse: StringDict) { - this.polygon = serverResponse["polygon"] as Polygon; + this.polygon = "polygon" in serverResponse ? new Polygon(...serverResponse["polygon"]) : null; this.page = "page" in serverResponse ? serverResponse["page"] : undefined; } diff --git a/src/v2/product/index.ts b/src/v2/product/index.ts index d56cccbb..4d082a1b 100644 --- a/src/v2/product/index.ts +++ b/src/v2/product/index.ts @@ -1,5 +1,5 @@ export { Classification, ClassificationResponse } from "./classification/index.js"; -export { Crop, CropResponse } from "./crop/index.js"; +export { Crop, CropResponse, CropItem } from "./crop/index.js"; export { Extraction, ExtractionResponse } from "./extraction/index.js"; export { Ocr, OcrResponse } from "./ocr/index.js"; export { Split, SplitResponse } from "./split/index.js"; diff --git a/src/v2/product/ocr/ocrParameters.ts b/src/v2/product/ocr/ocrParameters.ts index 8a17ac48..94bbfbc0 100644 --- a/src/v2/product/ocr/ocrParameters.ts +++ b/src/v2/product/ocr/ocrParameters.ts @@ -21,6 +21,6 @@ import { logger } from "@/logger.js"; export class OcrParameters extends BaseParameters { constructor(params: BaseParametersConstructor & {}) { super({ ...params }); - logger.debug("Ocr parameters initialized."); + logger.debug("OCR parameters initialized."); } } diff --git a/src/v2/product/ocr/ocrWord.ts b/src/v2/product/ocr/ocrWord.ts index aca6080e..faa264fa 100644 --- a/src/v2/product/ocr/ocrWord.ts +++ b/src/v2/product/ocr/ocrWord.ts @@ -14,7 +14,7 @@ export class OcrWord { constructor(serverResponse: StringDict) { this.content = serverResponse["content"]; - this.polygon = new Polygon(serverResponse["polygon"]); + this.polygon = new Polygon(...serverResponse["polygon"]); } toString(): string { diff --git a/tests/v2/product/crop.spec.ts b/tests/v2/product/crop.spec.ts index 5f74bf9e..954ee043 100644 --- a/tests/v2/product/crop.spec.ts +++ b/tests/v2/product/crop.spec.ts @@ -1,8 +1,9 @@ import { expect } from "chai"; import path from "node:path"; import { V2_PRODUCT_PATH } from "../../index.js"; -import { CropResponse } from "@/v2/product/index.js"; +import { CropResponse, CropItem } from "@/v2/product/index.js"; import { loadV2Response } from "./utils.js"; +import { Polygon } from "@/geometry/index.js"; describe("MindeeV2 - Crop Response", async () => { it("should load a single result", async () => { @@ -10,8 +11,34 @@ describe("MindeeV2 - Crop Response", async () => { CropResponse, path.join(V2_PRODUCT_PATH, "crop", "crop_single.json") ); + // Validate inference metadata + expect(response.inference.id).to.equal("12345678-1234-1234-1234-123456789abc"); + expect(response.inference.model.id).to.equal("test-model-id"); + + // Validate file metadata + expect(response.inference.file.name).to.equal("sample.jpeg"); + expect(response.inference.file.pageCount).to.equal(1); + expect(response.inference.file.mimeType).to.equal("image/jpeg"); + + // Validate crops const crops = response.inference.result.crops; expect(crops).to.be.an("array").that.has.lengthOf(1); + + const crop: CropItem = crops[0]; + expect(crop.objectType).to.equal("invoice"); + expect(crop.location.page).to.equal(0); + + const polygon: Polygon = crop.location.polygon!; + expect(polygon.length).to.equal(4); + expect(polygon.length).to.equal(4); + expect(polygon[0][0]).to.equal(0.15); + expect(polygon[0][1]).to.equal(0.254); + expect(polygon[1][0]).to.equal(0.85); + expect(polygon[1][1]).to.equal(0.254); + expect(polygon[2][0]).to.equal(0.85); + expect(polygon[2][1]).to.equal(0.947); + expect(polygon[3][0]).to.equal(0.15); + expect(polygon[3][1]).to.equal(0.947); }); it("should load multiple results", async () => { @@ -19,7 +46,46 @@ describe("MindeeV2 - Crop Response", async () => { CropResponse, path.join(V2_PRODUCT_PATH, "crop", "crop_multiple.json") ); - const crops = response.inference.result.crops; + // Validate inference metadata + expect(response.inference.id).to.equal("12345678-1234-1234-1234-123456789abc"); + expect(response.inference.model.id).to.equal("test-model-id"); + + // Validate file metadata + expect(response.inference.file.name).to.equal("default_sample.jpg"); + expect(response.inference.file.pageCount).to.equal(1); + expect(response.inference.file.mimeType).to.equal("image/jpeg"); + + const crops: CropItem[] = response.inference.result.crops; expect(crops).to.be.an("array").that.has.lengthOf(2); + + // Validate first crop item + const firstCrop: CropItem = crops[0]; + expect(firstCrop.objectType).to.equal("invoice"); + expect(firstCrop.location.page).to.equal(0); + const firstPolygon: Polygon = firstCrop.location.polygon!; + expect(firstPolygon.length).to.equal(4); + expect(firstPolygon[0][0]).to.equal(0.214); + expect(firstPolygon[0][1]).to.equal(0.079); + expect(firstPolygon[1][0]).to.equal(0.476); + expect(firstPolygon[1][1]).to.equal(0.079); + expect(firstPolygon[2][0]).to.equal(0.476); + expect(firstPolygon[2][1]).to.equal(0.979); + expect(firstPolygon[3][0]).to.equal(0.214); + expect(firstPolygon[3][1]).to.equal(0.979); + + // Validate second crop item + const secondCrop: CropItem = crops[1]; + expect(secondCrop.objectType).to.equal("invoice"); + expect(secondCrop.location.page).to.equal(0); + const secondPolygon: Polygon = secondCrop.location.polygon!; + expect(secondPolygon.length).to.equal(4); + expect(secondPolygon[0][0]).to.equal(0.547); + expect(secondPolygon[0][1]).to.equal(0.15); + expect(secondPolygon[1][0]).to.equal(0.862); + expect(secondPolygon[1][1]).to.equal(0.15); + expect(secondPolygon[2][0]).to.equal(0.862); + expect(secondPolygon[2][1]).to.equal(0.97); + expect(secondPolygon[3][0]).to.equal(0.547); + expect(secondPolygon[3][1]).to.equal(0.97); }); }); diff --git a/tests/v2/product/utils.ts b/tests/v2/product/utils.ts index d72c5253..a789ba1f 100644 --- a/tests/v2/product/utils.ts +++ b/tests/v2/product/utils.ts @@ -6,6 +6,5 @@ export async function loadV2Response( resourcePath: string ): Promise { const localResponse = new LocalResponse(resourcePath); - await localResponse.init(); return localResponse.deserializeResponse(responseClass); } From 30ba6fb14b9707afd379121baf7660a1f98d986f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Mon, 9 Feb 2026 13:42:30 +0100 Subject: [PATCH 7/7] test ocr --- src/v2/product/index.ts | 11 ++++- src/v2/product/ocr/index.ts | 3 ++ tests/v2/product/classification.spec.ts | 3 +- tests/v2/product/crop.spec.ts | 25 ++++++----- tests/v2/product/extraction.spec.ts | 3 +- .../extractionParameter.spec.ts} | 18 ++++---- tests/v2/product/ocr.spec.ts | 44 ++++++++++++++++--- tests/v2/product/split.spec.ts | 30 ++++++++++--- tests/v2/product/splitParameter.spec.ts | 36 +++++++++++++++ 9 files changed, 139 insertions(+), 34 deletions(-) rename tests/v2/{client/inferenceParameter.spec.ts => product/extractionParameter.spec.ts} (75%) create mode 100644 tests/v2/product/splitParameter.spec.ts diff --git a/src/v2/product/index.ts b/src/v2/product/index.ts index 4d082a1b..fe65c371 100644 --- a/src/v2/product/index.ts +++ b/src/v2/product/index.ts @@ -1,5 +1,14 @@ export { Classification, ClassificationResponse } from "./classification/index.js"; -export { Crop, CropResponse, CropItem } from "./crop/index.js"; +export * as classification from "./classification/index.js"; + +export { Crop, CropResponse } from "./crop/index.js"; +export * as crop from "./crop/index.js"; + export { Extraction, ExtractionResponse } from "./extraction/index.js"; +export * as extraction from "./extraction/index.js"; + export { Ocr, OcrResponse } from "./ocr/index.js"; +export * as ocr from "./ocr/index.js"; + export { Split, SplitResponse } from "./split/index.js"; +export * as split from "./split/index.js"; diff --git a/src/v2/product/ocr/index.ts b/src/v2/product/ocr/index.ts index 78caefa2..4b6fabc7 100644 --- a/src/v2/product/ocr/index.ts +++ b/src/v2/product/ocr/index.ts @@ -2,3 +2,6 @@ export { Ocr } from "./ocr.js"; export { OcrParameters } from "./ocrParameters.js"; export { OcrResponse } from "./ocrResponse.js"; export { OcrInference } from "./ocrInference.js"; +export { OcrResult } from "./ocrResult.js"; +export { OcrPage } from "./ocrPage.js"; +export { OcrWord } from "./ocrWord.js"; diff --git a/tests/v2/product/classification.spec.ts b/tests/v2/product/classification.spec.ts index 5ecc9d93..a8c5e64b 100644 --- a/tests/v2/product/classification.spec.ts +++ b/tests/v2/product/classification.spec.ts @@ -1,7 +1,8 @@ import { expect } from "chai"; import path from "node:path"; -import { V2_PRODUCT_PATH } from "../../index.js"; import { ClassificationResponse } from "@/v2/product/index.js"; + +import { V2_PRODUCT_PATH } from "../../index.js"; import { loadV2Response } from "./utils.js"; diff --git a/tests/v2/product/crop.spec.ts b/tests/v2/product/crop.spec.ts index 954ee043..b0e7ca8a 100644 --- a/tests/v2/product/crop.spec.ts +++ b/tests/v2/product/crop.spec.ts @@ -1,14 +1,15 @@ import { expect } from "chai"; import path from "node:path"; +import { Polygon } from "@/geometry/index.js"; +import { crop } from "@/v2/product/index.js"; + import { V2_PRODUCT_PATH } from "../../index.js"; -import { CropResponse, CropItem } from "@/v2/product/index.js"; import { loadV2Response } from "./utils.js"; -import { Polygon } from "@/geometry/index.js"; describe("MindeeV2 - Crop Response", async () => { it("should load a single result", async () => { const response = await loadV2Response( - CropResponse, + crop.CropResponse, path.join(V2_PRODUCT_PATH, "crop", "crop_single.json") ); // Validate inference metadata @@ -21,14 +22,14 @@ describe("MindeeV2 - Crop Response", async () => { expect(response.inference.file.mimeType).to.equal("image/jpeg"); // Validate crops - const crops = response.inference.result.crops; + const crops: crop.CropItem[] = response.inference.result.crops; expect(crops).to.be.an("array").that.has.lengthOf(1); - const crop: CropItem = crops[0]; - expect(crop.objectType).to.equal("invoice"); - expect(crop.location.page).to.equal(0); + const firstCrop = crops[0]; + expect(firstCrop.objectType).to.equal("invoice"); + expect(firstCrop.location.page).to.equal(0); - const polygon: Polygon = crop.location.polygon!; + const polygon: Polygon = firstCrop.location.polygon!; expect(polygon.length).to.equal(4); expect(polygon.length).to.equal(4); expect(polygon[0][0]).to.equal(0.15); @@ -43,7 +44,7 @@ describe("MindeeV2 - Crop Response", async () => { it("should load multiple results", async () => { const response = await loadV2Response( - CropResponse, + crop.CropResponse, path.join(V2_PRODUCT_PATH, "crop", "crop_multiple.json") ); // Validate inference metadata @@ -55,11 +56,11 @@ describe("MindeeV2 - Crop Response", async () => { expect(response.inference.file.pageCount).to.equal(1); expect(response.inference.file.mimeType).to.equal("image/jpeg"); - const crops: CropItem[] = response.inference.result.crops; + const crops: crop.CropItem[] = response.inference.result.crops; expect(crops).to.be.an("array").that.has.lengthOf(2); // Validate first crop item - const firstCrop: CropItem = crops[0]; + const firstCrop: crop.CropItem = crops[0]; expect(firstCrop.objectType).to.equal("invoice"); expect(firstCrop.location.page).to.equal(0); const firstPolygon: Polygon = firstCrop.location.polygon!; @@ -74,7 +75,7 @@ describe("MindeeV2 - Crop Response", async () => { expect(firstPolygon[3][1]).to.equal(0.979); // Validate second crop item - const secondCrop: CropItem = crops[1]; + const secondCrop: crop.CropItem = crops[1]; expect(secondCrop.objectType).to.equal("invoice"); expect(secondCrop.location.page).to.equal(0); const secondPolygon: Polygon = secondCrop.location.polygon!; diff --git a/tests/v2/product/extraction.spec.ts b/tests/v2/product/extraction.spec.ts index ac7f3855..6a290fcd 100644 --- a/tests/v2/product/extraction.spec.ts +++ b/tests/v2/product/extraction.spec.ts @@ -9,8 +9,9 @@ import { SimpleField, } from "@/v2/parsing/inference/field/index.js"; import { field } from "@/v2/parsing/index.js"; -import { V2_PRODUCT_PATH } from "../../index.js"; import { ExtractionResponse } from "@/v2/product/index.js"; + +import { V2_PRODUCT_PATH } from "../../index.js"; import { loadV2Response } from "./utils.js"; const findocPath = path.join(V2_PRODUCT_PATH, "extraction", "financial_document"); diff --git a/tests/v2/client/inferenceParameter.spec.ts b/tests/v2/product/extractionParameter.spec.ts similarity index 75% rename from tests/v2/client/inferenceParameter.spec.ts rename to tests/v2/product/extractionParameter.spec.ts index 85c5ee7b..ce554c17 100644 --- a/tests/v2/client/inferenceParameter.spec.ts +++ b/tests/v2/product/extractionParameter.spec.ts @@ -3,19 +3,19 @@ import path from "path"; import { V2_PRODUCT_PATH } from "../../index.js"; import { expect } from "chai"; import { promises as fs } from "fs"; -import { ExtractionParameters, DataSchema } from "@/v2/product/extraction/index.js"; +import { extraction } from "@/v2/product/index.js"; let expectedDataSchemaDict: StringDict; let expectedDataSchemaString: string; -let expectedDataSchemaObject: DataSchema; +let expectedDataSchemaObject: extraction.DataSchema; -describe("MindeeV2 - Inference Parameter", () => { +describe("MindeeV2 - Extraction Parameter", () => { const modelIdValue = "test-model-id"; describe("Polling Options", () => { it("should provide sensible defaults", () => { - const paramsInstance = new ExtractionParameters({ + const paramsInstance = new extraction.ExtractionParameters({ modelId: modelIdValue, }); expect(paramsInstance.modelId).to.equal(modelIdValue); @@ -34,26 +34,26 @@ describe("MindeeV2 - Inference Parameter", () => { ); expectedDataSchemaDict = JSON.parse(fileContents.toString()); expectedDataSchemaString = JSON.stringify(expectedDataSchemaDict); - expectedDataSchemaObject = new DataSchema(expectedDataSchemaDict); + expectedDataSchemaObject = new extraction.DataSchema(expectedDataSchemaDict); }); it("shouldn't replace when unset", () => { - const params = new ExtractionParameters({ + const params = new extraction.ExtractionParameters({ modelId: modelIdValue, }); expect(params.dataSchema).to.be.undefined; }); it("should equate no matter the type", () => { - const paramsDict = new ExtractionParameters({ + const paramsDict = new extraction.ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaDict, }); - const paramsString = new ExtractionParameters({ + const paramsString = new extraction.ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaString, }); - const paramsObject = new ExtractionParameters({ + const paramsObject = new extraction.ExtractionParameters({ modelId: modelIdValue, dataSchema: expectedDataSchemaObject, }); diff --git a/tests/v2/product/ocr.spec.ts b/tests/v2/product/ocr.spec.ts index 7cb19658..4135024c 100644 --- a/tests/v2/product/ocr.spec.ts +++ b/tests/v2/product/ocr.spec.ts @@ -1,26 +1,60 @@ import { expect } from "chai"; import path from "node:path"; +import { ocr } from "@/v2/product/index.js"; +import { Polygon } from "@/geometry/index.js"; + import { V2_PRODUCT_PATH } from "../../index.js"; -import { OcrResponse } from "@/v2/product/index.js"; import { loadV2Response } from "./utils.js"; describe("MindeeV2 - OCR Response", async () => { it("should load a single result", async () => { const response = await loadV2Response( - OcrResponse, + ocr.OcrResponse, path.join(V2_PRODUCT_PATH, "ocr", "ocr_single.json") ); - const pages = response.inference.result.pages; + // Validate inference metadata + expect(response.inference.id).to.equal("12345678-1234-1234-1234-123456789abc"); + expect(response.inference.model.id).to.equal("test-model-id"); + + // Validate file metadata + expect(response.inference.file.name).to.equal("default_sample.jpg"); + expect(response.inference.file.pageCount).to.equal(1); + expect(response.inference.file.mimeType).to.equal("image/jpeg"); + + // Validate pages + const pages: ocr.OcrPage[] = response.inference.result.pages; expect(pages).to.be.an("array").that.has.lengthOf(1); + + // Validate first page + const firstPage: ocr.OcrPage = pages[0]; + expect(firstPage.words).to.be.an("array"); + + // Check first word + const firstWord: ocr.OcrWord = firstPage.words[0]; + expect(firstWord.content).to.equal("Shipper:"); + const firstPolygon: Polygon = firstWord.polygon; + expect(firstPolygon.length).to.equal(4); + + // Check another word (5th word: "INC.") + const fifthWord: ocr.OcrWord = firstPage.words[4]; + expect(fifthWord.content).to.equal("INC."); + const fifthPolygon: Polygon = fifthWord.polygon; + expect(fifthPolygon.length).to.equal(4); }); it("should load multiple results", async () => { const response = await loadV2Response( - OcrResponse, + ocr.OcrResponse, path.join(V2_PRODUCT_PATH, "ocr", "ocr_multiple.json") ); - const pages = response.inference.result.pages; + const pages: ocr.OcrPage[] = response.inference.result.pages; expect(pages).to.be.an("array").that.has.lengthOf(3); + + // Validate that each page has words and content + pages.forEach((page: ocr.OcrPage): void => { + expect(page.words).to.be.an("array"); + expect(page.content).to.be.a("string"); + }); }); }); diff --git a/tests/v2/product/split.spec.ts b/tests/v2/product/split.spec.ts index c79f234b..27042865 100644 --- a/tests/v2/product/split.spec.ts +++ b/tests/v2/product/split.spec.ts @@ -1,26 +1,46 @@ import { expect } from "chai"; import path from "node:path"; +import { split } from "@/v2/product/index.js"; + import { V2_PRODUCT_PATH } from "../../index.js"; -import { SplitResponse } from "@/v2/product/index.js"; import { loadV2Response } from "./utils.js"; describe("MindeeV2 - Split Response", async () => { it("should load a single result", async () => { const response = await loadV2Response( - SplitResponse, + split.SplitResponse, path.join(V2_PRODUCT_PATH, "split", "split_single.json") ); - const splits = response.inference.result.splits; + const splits: split.SplitRange[] = response.inference.result.splits; expect(splits).to.be.an("array").that.has.lengthOf(1); + + const firstSplit: split.SplitRange = splits[0]; + expect(firstSplit.documentType).to.equal("receipt"); + + expect(firstSplit.pageRange).to.be.an("array").that.has.lengthOf(2); + expect(firstSplit.pageRange[0]).to.equal(0); + expect(firstSplit.pageRange[1]).to.equal(0); }); it("should load multiple results", async () => { const response = await loadV2Response( - SplitResponse, + split.SplitResponse, path.join(V2_PRODUCT_PATH, "split", "split_multiple.json") ); - const splits = response.inference.result.splits; + const splits: split.SplitRange[] = response.inference.result.splits; expect(splits).to.be.an("array").that.has.lengthOf(3); + + const firstSplit: split.SplitRange = splits[0]; + expect(firstSplit.documentType).to.equal("invoice"); + expect(firstSplit.pageRange).to.be.an("array").that.has.lengthOf(2); + expect(firstSplit.pageRange[0]).to.equal(0); + expect(firstSplit.pageRange[1]).to.equal(0); + + const secondSplit: split.SplitRange = splits[1]; + expect(secondSplit.documentType).to.equal("invoice"); + expect(secondSplit.pageRange).to.be.an("array").that.has.lengthOf(2); + expect(secondSplit.pageRange[0]).to.equal(1); + expect(secondSplit.pageRange[1]).to.equal(3); }); }); diff --git a/tests/v2/product/splitParameter.spec.ts b/tests/v2/product/splitParameter.spec.ts new file mode 100644 index 00000000..83aee3dd --- /dev/null +++ b/tests/v2/product/splitParameter.spec.ts @@ -0,0 +1,36 @@ +import { expect } from "chai"; +import { split } from "@/v2/product/index.js"; + +describe("MindeeV2 - Split Parameter", () => { + const modelIdValue = "test-model-id"; + + describe("Polling Options", () => { + it("should provide sensible defaults", () => { + + const paramsInstance = new split.SplitParameters({ + modelId: modelIdValue, + }); + expect(paramsInstance.modelId).to.equal(modelIdValue); + expect(paramsInstance.getValidatedPollingOptions()).to.deep.equal({ + delaySec: 1.5, + initialDelaySec: 2, + maxRetries: 80 + }); + }); + }); + + describe("Invalid Options", () => { + it("should not set invalid options", () => { + + const paramsInstance = new split.SplitParameters({ + modelId: modelIdValue, + // @ts-expect-error - rag is not a valid option + rag: true, + }); + expect(paramsInstance.modelId).to.equal(modelIdValue); + // @ts-expect-error - rag is not a valid option + expect(paramsInstance.rag).to.be.undefined; + }); + }); + +});