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.
347 lines
12 KiB
JavaScript
347 lines
12 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { saveSession, loadSession, clearSession } from '../src/storage.js';
|
|
import {
|
|
createSession,
|
|
commitAssignment,
|
|
proposeAssignment,
|
|
moveEntry,
|
|
removeEntry,
|
|
} from '../src/session.js';
|
|
import { emptySpec } from '../src/spec.js';
|
|
|
|
function fakeStore() {
|
|
const data = new Map();
|
|
return {
|
|
data,
|
|
getItem: (key) => (data.has(key) ? data.get(key) : null),
|
|
setItem: (key, value) => data.set(key, value),
|
|
removeItem: (key) => data.delete(key),
|
|
};
|
|
}
|
|
|
|
test('leerer Speicher liefert null', () => {
|
|
assert.equal(loadSession(fakeStore()), null);
|
|
});
|
|
|
|
test('Sitzung ueberlebt Sichern und Laden', () => {
|
|
const store = fakeStore();
|
|
const session = createSession();
|
|
commitAssignment(session, { ...emptySpec(), capacityGb: 64 }, 'barcode', 'A');
|
|
saveSession(session, store);
|
|
|
|
const reloaded = loadSession(store);
|
|
assert.equal(reloaded.stacks.length, 1);
|
|
assert.equal(reloaded.stacks[0].count, 1);
|
|
assert.equal(reloaded.entries[0].spec.capacityGb, 64);
|
|
assert.equal(reloaded.nextEntryId, 2);
|
|
});
|
|
|
|
test('clearSession raeumt auf', () => {
|
|
const store = fakeStore();
|
|
saveSession(createSession(), store);
|
|
clearSession(store);
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('kaputter Inhalt liefert null statt einer Ausnahme', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', '{kein json');
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Sichern ohne funktionierenden Speicher wirft nicht', () => {
|
|
const broken = {
|
|
getItem: () => null,
|
|
setItem: () => { throw new Error('quota exceeded'); },
|
|
removeItem: () => {},
|
|
};
|
|
assert.doesNotThrow(() => saveSession(createSession(), broken));
|
|
});
|
|
|
|
test('stacks mit Nicht-Objekten liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: ['x', 'y'],
|
|
entries: [],
|
|
nextEntryId: 1,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('fehlender nextEntryId liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [],
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('nextEntryId als Zeichenkette liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [],
|
|
nextEntryId: '2',
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Stapel mit Zaehler als Zeichenkette liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: '3', spec: emptySpec() }],
|
|
entries: [],
|
|
nextEntryId: 1,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Eintrag ohne Spec liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [{ entryId: 1, stackId: 'A' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Stapelzaehler wird beim Laden aus den Eintraegen neu berechnet', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 5, spec: emptySpec() }],
|
|
entries: [{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null);
|
|
assert.equal(reloaded.stacks[0].count, 1);
|
|
});
|
|
|
|
test('Stapelzaehler werden beim Laden fuer mehrere Stapel unabhaengig neu berechnet, auch fuer einen ohne Eintraege', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [
|
|
{ id: 'A', count: 99, spec: emptySpec() },
|
|
{ id: 'B', count: 99, spec: emptySpec() },
|
|
{ id: 'C', count: 99, spec: emptySpec() },
|
|
],
|
|
entries: [
|
|
{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode' },
|
|
{ entryId: 2, stackId: 'A', spec: emptySpec(), source: 'barcode' },
|
|
{ entryId: 3, stackId: 'B', spec: emptySpec(), source: 'ocr' },
|
|
],
|
|
nextEntryId: 4,
|
|
}));
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null);
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'A').count, 2, 'Stapel A hat zwei zugeordnete Eintraege');
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'B').count, 1, 'Stapel B hat einen zugeordneten Eintrag');
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'C').count, 0, 'Stapel C ohne jeden Eintrag wird auf 0 neu berechnet, nicht auf den gespeicherten Wert');
|
|
});
|
|
|
|
test('Eintrag mit unbekannter Stapelkennung liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [{ entryId: 1, stackId: 'Z', spec: emptySpec(), source: 'barcode' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('doppelte Eintragsnummern liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 2, spec: emptySpec() }],
|
|
entries: [
|
|
{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode' },
|
|
{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode' },
|
|
],
|
|
nextEntryId: 2,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('doppelte Stapelkennungen liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [
|
|
{ id: 'A', count: 0, spec: emptySpec() },
|
|
{ id: 'A', count: 0, spec: emptySpec() },
|
|
],
|
|
entries: [],
|
|
nextEntryId: 1,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('nextEntryId nicht groesser als vorhandene Eintragsnummer liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 1, spec: emptySpec() }],
|
|
entries: [{ entryId: 5, stackId: 'A', spec: emptySpec(), source: 'barcode' }],
|
|
nextEntryId: 5,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('nach Sichern und Laden funktioniert Weiterarbeiten weiterhin', () => {
|
|
const store = fakeStore();
|
|
const session = createSession();
|
|
const spec = { ...emptySpec(), capacityGb: 64 };
|
|
commitAssignment(session, spec, 'barcode', 'A');
|
|
const secondEntry = commitAssignment(session, spec, 'barcode', 'A');
|
|
saveSession(session, store);
|
|
|
|
const reloaded = loadSession(store);
|
|
|
|
const proposal = proposeAssignment(reloaded, spec);
|
|
assert.equal(proposal.kind, 'match');
|
|
assert.equal(proposal.stackId, 'A');
|
|
commitAssignment(reloaded, spec, 'barcode', proposal.stackId);
|
|
assert.equal(reloaded.stacks[0].count, 3);
|
|
assert.equal(reloaded.nextEntryId, 4);
|
|
|
|
moveEntry(reloaded, secondEntry.entryId, 'B');
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'A').count, 2);
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'B').count, 1);
|
|
|
|
removeEntry(reloaded, secondEntry.entryId);
|
|
assert.equal(reloaded.entries.find((e) => e.entryId === secondEntry.entryId), undefined);
|
|
assert.equal(reloaded.stacks.find((s) => s.id === 'B'), undefined);
|
|
});
|
|
|
|
test('nextEntryId als negative Zahl liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [],
|
|
nextEntryId: -5,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('nextEntryId als gebrochene Zahl liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [],
|
|
nextEntryId: 1.5,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('nextEntryId als Null liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [],
|
|
entries: [],
|
|
nextEntryId: 0,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
// --- Erweiterung: Barcode-Inhalte je Stapel ueberleben Sichern und Laden ---
|
|
|
|
test('Codes eines Stapels ueberleben Sichern und Laden', () => {
|
|
const store = fakeStore();
|
|
const session = createSession();
|
|
commitAssignment(
|
|
session,
|
|
{ ...emptySpec(), partNumber: 'HMA84GL7AFR4N-UH' },
|
|
'barcode',
|
|
'A',
|
|
['HMA84GL7AFR4N-UH', 'SN-000'],
|
|
);
|
|
saveSession(session, store);
|
|
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null);
|
|
assert.deepEqual([...reloaded.stacks[0].codes].sort(), ['HMA84GL7AFR4N-UH', 'SN-000'].sort());
|
|
|
|
const plan = proposeAssignment(reloaded, emptySpec(), ['HMA84GL7AFR4N-UH']);
|
|
assert.equal(plan.kind, 'match');
|
|
assert.equal(plan.stackId, 'A');
|
|
});
|
|
|
|
test('ein Stand ohne codes-Feld (aeltere Fassung) wird nicht verworfen, der Stapel gilt als ohne bekannte Codes', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 1, spec: { ...emptySpec(), capacityGb: 64 } }],
|
|
entries: [{ entryId: 1, stackId: 'A', spec: { ...emptySpec(), capacityGb: 64 }, source: 'barcode' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null, 'ein Stand ohne codes-Feld darf nicht verworfen werden');
|
|
assert.deepEqual(reloaded.stacks[0].codes, []);
|
|
});
|
|
|
|
test('Stapel mit codes als Nicht-Array liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 0, spec: emptySpec(), codes: 'nicht-array' }],
|
|
entries: [],
|
|
nextEntryId: 1,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Stapel mit codes-Array, das Nicht-Zeichenketten enthaelt, liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 0, spec: emptySpec(), codes: [123] }],
|
|
entries: [],
|
|
nextEntryId: 1,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
test('Eintrag mit gebrochener entryId liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 1, spec: emptySpec() }],
|
|
entries: [{ entryId: 1.5, stackId: 'A', spec: emptySpec(), source: 'barcode' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|
|
|
|
// --- Erweiterung: der OCR-Rohtext eines Eintrags (entry.rawText) ---
|
|
|
|
test('Rohtext eines Eintrags ueberlebt Sichern und Laden', () => {
|
|
const store = fakeStore();
|
|
const session = createSession();
|
|
const entry = commitAssignment(session, { ...emptySpec(), capacityGb: 32 }, 'ocr', 'A');
|
|
entry.rawText = '32GB 2Rx4 PC4-2666 Vollständiges Etikett Made in Korea';
|
|
saveSession(session, store);
|
|
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null);
|
|
assert.equal(reloaded.entries[0].rawText, '32GB 2Rx4 PC4-2666 Vollständiges Etikett Made in Korea');
|
|
});
|
|
|
|
test('ein Stand ohne rawText-Feld (aeltere Fassung) wird nicht verworfen, der Eintrag gilt als ohne Rohtext', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 1, spec: { ...emptySpec(), capacityGb: 64 } }],
|
|
entries: [{ entryId: 1, stackId: 'A', spec: { ...emptySpec(), capacityGb: 64 }, source: 'barcode' }],
|
|
nextEntryId: 2,
|
|
}));
|
|
const reloaded = loadSession(store);
|
|
assert.notEqual(reloaded, null, 'ein Stand ohne rawText-Feld darf nicht verworfen werden');
|
|
assert.equal(reloaded.entries[0].rawText, '');
|
|
});
|
|
|
|
test('Eintrag mit rawText als Nicht-Zeichenkette liefert null', () => {
|
|
const store = fakeStore();
|
|
store.setItem('ram-sortierhilfe:session', JSON.stringify({
|
|
stacks: [{ id: 'A', count: 1, spec: emptySpec() }],
|
|
entries: [{ entryId: 1, stackId: 'A', spec: emptySpec(), source: 'barcode', rawText: 123 }],
|
|
nextEntryId: 2,
|
|
}));
|
|
assert.equal(loadSession(store), null);
|
|
});
|