recognize() (pipeline.js) liefert zusätzlich den ungefilterten OCR-Rohtext (rawText) zurück - leer, wenn keine Texterkennung lief. Rein additiv: die Stapelzuordnung (session.js/spec.js, unangetastet) stützt sich weiterhin ausschließlich auf die daraus abgeleiteten, verstandenen Spec-Felder. main.js hängt den Rohtext nach dem Buchen als zusätzliche Eigenschaft an den Eintrag (analog zu den bereits vorhandenen Barcode-Inhalten je Eintrag). storage.js sichert/prüft/stellt entry.rawText nach demselben optionalen Muster wie entry.codes wieder her - ein gesicherter Stand ohne dieses Feld wird nicht verworfen. In Funktion "Text erkennen" bleibt die Treffer-Rückmeldung jetzt stehen, bis der Nutzer sie wegtippt, und zeigt den vollen erkannten Text (lesbar über dasselbe Scroll-Muster wie die übrigen Vollbild-Overlays); in den beiden Barcode-Funktionen bleibt es beim automatischen Ausblenden. Das zurückgegebene Versprechen von showResult löst in jedem Fall - Zeitgeber, Wegtippen, verdrängende neue Rückmeldung - genau einmal ein, damit die Bedienung nie dauerhaft gesperrt bleibt. Die Sitzungsliste zeigt den Rohtext zusätzlich je Eintrag zum Nachlesen. Tests (pipeline.test.js, storage.test.js): rawText nur bei tatsächlich gelaufener Texterkennung, unterschiedlicher Rohtext beeinflusst die Stapelzuordnung nachweislich nicht, Rohtext übersteht Sichern/Laden inkl. Abwärtskompatibilität zu einem Stand ohne dieses Feld.
375 lines
16 KiB
JavaScript
375 lines
16 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { recognize } from '../src/pipeline.js';
|
|
import { emptySpec } from '../src/spec.js';
|
|
import { createSession, proposeAssignment, commitAssignment } from '../src/session.js';
|
|
|
|
const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
|
|
|
const deps = ({ codes = [], text = '' }) => ({
|
|
decodeBarcodes: async () => codes,
|
|
runOcr: async () => text,
|
|
});
|
|
|
|
test('Barcode mit bekannter Teilenummer ergibt gruen und ueberspringt OCR', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
});
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.confidence, 'green');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
assert.equal(ocrCalled, false, 'OCR darf bei gruenem Barcode nicht laufen');
|
|
});
|
|
|
|
test('Barcode ohne bekannte Teilenummer faellt auf OCR zurueck', async () => {
|
|
const result = await recognize({}, deps({ codes: ['7325773'], text: OCR_TEXT }));
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('OCR-Ergebnis behaelt die Teilenummer aus dem Barcode bei', async () => {
|
|
const result = await recognize({}, deps({ codes: ['7325773'], text: '64GB 4DRx4 PC4-2400' }));
|
|
assert.equal(result.spec.partNumber, '7325773');
|
|
});
|
|
|
|
test('ohne Barcode und ohne brauchbaren Text ergibt rot', async () => {
|
|
const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' }));
|
|
assert.equal(result.source, 'none');
|
|
assert.equal(result.confidence, 'red');
|
|
});
|
|
|
|
test('OCR-Fehler fuehrt zu rot statt zu einem Absturz', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => [],
|
|
runOcr: async () => { throw new Error('tesseract nicht geladen'); },
|
|
});
|
|
assert.equal(result.confidence, 'red');
|
|
assert.equal(result.source, 'none');
|
|
});
|
|
|
|
test('Barcode-Fehler fuehrt nicht zum Abbruch, OCR uebernimmt', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => { throw new Error('zxing nicht geladen'); },
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
// --- Beanstandung 1: unerwartete Rueckgabeform statt eines Fehlers ---
|
|
|
|
test('decodeBarcodes liefert null statt einer Liste, kein Absturz', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => null,
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('decodeBarcodes liefert undefined statt einer Liste, kein Absturz', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => undefined,
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('decodeBarcodes liefert ein Objekt statt einer Liste, kein Absturz', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ({ notAList: true }),
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('runOcr liefert keine Zeichenkette, wird wie leerer Text behandelt', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => [],
|
|
runOcr: async () => ({ unexpected: true }),
|
|
});
|
|
assert.equal(result.source, 'none');
|
|
assert.equal(result.confidence, 'red');
|
|
});
|
|
|
|
// --- Beanstandung 2: mehrere verwertbare Barcodes im selben Bild ---
|
|
|
|
test('mehrere unvertraegliche Barcodes ergeben rot statt eines ungeprueften ersten Treffers', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
// Gleiche Bauform und Dichte, aber widersprechende Geschwindigkeit:
|
|
// zwei unterschiedliche Module im selben Bild.
|
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
});
|
|
assert.equal(result.source, 'none');
|
|
assert.equal(result.confidence, 'red');
|
|
assert.deepEqual(result.spec, emptySpec());
|
|
assert.equal(ocrCalled, false, 'bei mehrdeutigem Barcode-Ergebnis soll nicht geraten werden');
|
|
});
|
|
|
|
test('mehrere untereinander vertraegliche Barcodes ergeben gruen (der erste Treffer entscheidet, es gibt nichts zusammenzufuehren)', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
// Zwei Lesungen desselben Etiketts (z.B. Mehrfachtreffer durch
|
|
// Spiegelung) - inhaltlich identisch, nur die Schreibweise weicht
|
|
// an einer verwechslungstoleranten Stelle ab (B/8).
|
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K408M1-CRC4Y'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
});
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.confidence, 'green');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
assert.equal(result.spec.formFactor, 'LRDIMM');
|
|
assert.equal(result.spec.rank, '4DRx4');
|
|
assert.equal(result.spec.speed, 'PC4-2400');
|
|
assert.equal(ocrCalled, false, 'OCR darf bei gruenem Mehrfachtreffer nicht laufen');
|
|
});
|
|
|
|
// --- Beanstandung 1: haengender Adapter darf die Erkennung nicht dauerhaft blockieren ---
|
|
|
|
test('haengender Barcode-Adapter blockiert nicht dauerhaft, OCR uebernimmt', { timeout: 500 }, async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: () => new Promise(() => {}), // loest nie ein und wirft nie
|
|
runOcr: async () => OCR_TEXT,
|
|
barcodeTimeoutMs: 20,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('haengender OCR-Adapter blockiert nicht dauerhaft, Erkennung ergibt rot', { timeout: 500 }, async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => [],
|
|
runOcr: () => new Promise(() => {}), // loest nie ein und wirft nie
|
|
ocrTimeoutMs: 20,
|
|
});
|
|
assert.equal(result.source, 'none');
|
|
assert.equal(result.confidence, 'red');
|
|
});
|
|
|
|
// --- Beanstandung 1 (3. Runde): Mehrdeutigkeit bei unverwertbaren Barcodes ---
|
|
|
|
test('zwei widersprechende unverwertbare Barcodes: keiner erhaelt Vorrang, gelesene Teilenummer bleibt', async () => {
|
|
const result = await recognize({}, {
|
|
// Beide ohne bekanntes Nummernschema, und sie widersprechen sich -
|
|
// keiner darf die aus dem Klartext gelesene Teilenummer ueberschreiben.
|
|
decodeBarcodes: async () => ['7325773', '9999999'],
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.partNumber, 'M386A8K40BM1-CRC4Y');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
});
|
|
|
|
test('zwei unverwertbare Barcodes mit gleicher Teilenummer: Vorrang bleibt bestehen', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['7325773', '7325773'],
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.partNumber, '7325773');
|
|
});
|
|
|
|
test('zwei unverwertbare Barcodes, die sich nur in einer Verwechslung unterscheiden, gelten als derselbe', async () => {
|
|
const result = await recognize({}, {
|
|
// 'O' und '0' sind eine typische Verwechslung (siehe CONFUSIONS in spec.js).
|
|
decodeBarcodes: async () => ['732O773', '7320773'],
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.partNumber, '732O773');
|
|
});
|
|
|
|
// --- Erweiterung: unbekanntes Nummernschema mit gelesener Teilenummer genuegt fuer Gruen ---
|
|
|
|
test('Barcode mit unbekanntem Nummernschema und gelesener Teilenummer ergibt gruen, auch ohne Kapazitaet', async () => {
|
|
const result = await recognize({}, {
|
|
// Hynix-Teilenummer: Schema unbekannt (nur Samsung ist hinterlegt), aber
|
|
// exakt gelesen - und OCR findet auf dem Etikett ebenfalls nichts
|
|
// Verwertbares. Trotzdem genuegt die gelesene Teilenummer fuer Gruen.
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => '',
|
|
});
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.confidence, 'green');
|
|
assert.equal(result.spec.partNumber, 'HMA84GL7AFR4N-UH');
|
|
assert.equal(result.spec.capacityGb, null);
|
|
});
|
|
|
|
test('findet OCR trotz unbekanntem Nummernschema eine Kapazitaet, wird sie uebernommen (weiterhin gelb, nicht gruen)', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => OCR_TEXT,
|
|
});
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.confidence, 'yellow');
|
|
assert.equal(result.spec.capacityGb, 64);
|
|
// Die Teilenummer aus dem Barcode ist exakter und bleibt erhalten.
|
|
assert.equal(result.spec.partNumber, 'HMA84GL7AFR4N-UH');
|
|
});
|
|
|
|
test('widersprechende unverwertbare Barcodes ohne jede OCR-Kapazitaet: die Vorsicht bleibt erhalten, kein Gruen', () => recognize({}, {
|
|
decodeBarcodes: async () => ['7325773', '9999999'],
|
|
runOcr: async () => '',
|
|
}).then((result) => {
|
|
assert.equal(result.source, 'none');
|
|
assert.equal(result.confidence, 'red');
|
|
}));
|
|
|
|
test('recognize liefert die gelesenen Rohcodes zur Weitergabe an die Stapelzuordnung', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH', 'SN-000123'],
|
|
runOcr: async () => '',
|
|
});
|
|
assert.deepEqual([...result.codes].sort(), ['HMA84GL7AFR4N-UH', 'SN-000123'].sort());
|
|
});
|
|
|
|
// --- Erweiterung: ein bekannter Code ueberspringt die Texterkennung ---
|
|
|
|
test('bekannter Code ueberspringt die Texterkennung vollstaendig', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH',
|
|
});
|
|
assert.equal(ocrCalled, false, 'ein bekannter Code macht die Texterkennung ueberfluessig');
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.confidence, 'green');
|
|
});
|
|
|
|
test('bekannter Code unter mehreren Codes ueberspringt die Texterkennung trotzdem (Seriennummer laeuft ins Leere)', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH', 'SN-000123'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH',
|
|
});
|
|
assert.equal(ocrCalled, false);
|
|
assert.equal(result.confidence, 'green');
|
|
});
|
|
|
|
test('unbekannter Code (isKnownCode liefert stets false) ueberspringt die Texterkennung nicht', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
isKnownCode: () => false,
|
|
});
|
|
assert.equal(ocrCalled, true);
|
|
assert.equal(result.confidence, 'green');
|
|
});
|
|
|
|
test('ohne isKnownCode-Abhaengigkeit bleibt die Pipeline ohne sie lauffaehig (Texterkennung laeuft wie bisher)', async () => {
|
|
let ocrCalled = false;
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => { ocrCalled = true; return ''; },
|
|
});
|
|
assert.equal(ocrCalled, true);
|
|
assert.equal(result.confidence, 'green');
|
|
});
|
|
|
|
// --- Erweiterung: Rohtext als additives Feld (Funktion "Text erkennen") ---
|
|
//
|
|
// Der Rohtext ist reine Anzeige-Information fuer den Nutzer - er verlaesst
|
|
// die Erkennung, wird aber nirgends zur Stapelzuordnung herangezogen (siehe
|
|
// den letzten Test unten, der recognize() mit session.js zusammenspielen
|
|
// laesst).
|
|
|
|
test('recognize liefert den Rohtext, wenn die Texterkennung gelaufen ist (gelbes Ergebnis)', async () => {
|
|
const result = await recognize({}, deps({ codes: [], text: OCR_TEXT }));
|
|
assert.equal(result.source, 'ocr');
|
|
assert.equal(result.rawText, OCR_TEXT);
|
|
});
|
|
|
|
test('recognize liefert den Rohtext auch, wenn die Texterkennung lief, aber nichts Verwertbares fand (rot)', async () => {
|
|
const result = await recognize({}, deps({ codes: [], text: 'Made in Philippines' }));
|
|
assert.equal(result.confidence, 'red');
|
|
assert.equal(result.rawText, 'Made in Philippines');
|
|
});
|
|
|
|
test('recognize liefert den Rohtext, wenn OCR keine Kapazitaet findet, aber eine unverwertbare Barcode-Teilenummer bestaetigt (gruen)', async () => {
|
|
// '7325773' hat kein bekanntes Nummernschema (decodePartNumber liefert
|
|
// keine Kapazitaet) - die Texterkennung laeuft trotzdem und liefert Text
|
|
// ohne jede Kapazitaetsangabe. Die Teilenummer aus dem Barcode traegt
|
|
// (siehe recognize()), das Ergebnis bleibt gruen - der gelaufene Rohtext
|
|
// wird trotzdem zurueckgegeben.
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['7325773'],
|
|
runOcr: async () => 'Made in Philippines',
|
|
});
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.confidence, 'green');
|
|
assert.equal(result.spec.partNumber, '7325773');
|
|
assert.equal(result.rawText, 'Made in Philippines');
|
|
});
|
|
|
|
test('recognize liefert eine leere Zeichenkette als Rohtext, wenn die Texterkennung nicht lief (gruener Barcode-Treffer)', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
|
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
|
});
|
|
assert.equal(result.source, 'barcode');
|
|
assert.equal(result.rawText, '');
|
|
});
|
|
|
|
test('recognize liefert eine leere Zeichenkette als Rohtext, wenn ein bekannter Code die Texterkennung ueberfluessig macht', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['HMA84GL7AFR4N-UH'],
|
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
|
isKnownCode: (code) => code === 'HMA84GL7AFR4N-UH',
|
|
});
|
|
assert.equal(result.rawText, '');
|
|
});
|
|
|
|
test('recognize liefert eine leere Zeichenkette als Rohtext bei mehrdeutigen verwertbaren Barcodes (rot, ohne OCR)', async () => {
|
|
const result = await recognize({}, {
|
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'],
|
|
runOcr: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
|
});
|
|
assert.equal(result.confidence, 'red');
|
|
assert.equal(result.rawText, '');
|
|
});
|
|
|
|
test('der Rohtext beeinflusst die Stapelzuordnung nicht: zwei Aufnahmen mit unterschiedlichem Rohtext, aber gleichen verstandenen Feldern, landen auf demselben Stapel', async () => {
|
|
const firstText = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908';
|
|
// Anderer Rohtext (andere Reihenfolge, zusaetzliches "Made in Korea",
|
|
// andere Gross-/Kleinschreibung) - dieselben verstandenen Felder.
|
|
const secondText = 'made in korea m386a8k40bm1-crc4y 64gb 4drx4 PC4-2400 1908';
|
|
|
|
const first = await recognize({}, deps({ codes: [], text: firstText }));
|
|
const second = await recognize({}, deps({ codes: [], text: secondText }));
|
|
|
|
assert.notEqual(first.rawText, second.rawText, 'Vorbedingung: die Rohtexte unterscheiden sich tatsaechlich');
|
|
assert.deepEqual(first.spec, second.spec, 'Vorbedingung: dieselben verstandenen Felder');
|
|
|
|
const session = createSession();
|
|
const plan1 = proposeAssignment(session, first.spec, first.codes);
|
|
const entry1 = commitAssignment(session, first.spec, first.source, plan1.stackId, first.codes);
|
|
// Der Rohtext haengt (wie main.js es tut) rein additiv am Eintrag, ohne
|
|
// dass session.js davon weiss - siehe main.js/processCapture().
|
|
entry1.rawText = first.rawText;
|
|
|
|
const plan2 = proposeAssignment(session, second.spec, second.codes);
|
|
assert.equal(plan2.kind, 'match', 'das zweite Modul muss denselben Stapel treffen');
|
|
const entry2 = commitAssignment(session, second.spec, second.source, plan2.stackId, second.codes);
|
|
entry2.rawText = second.rawText;
|
|
|
|
assert.equal(entry1.stackId, entry2.stackId, 'unterschiedlicher Rohtext darf keinen eigenen Stapel erzeugen');
|
|
assert.notEqual(entry1.rawText, entry2.rawText, 'die Eintraege fuehren trotzdem je ihren eigenen Rohtext');
|
|
});
|