Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
115
tests/e2e/sprint_c_image.test.ts
Normal file
115
tests/e2e/sprint_c_image.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Sprint C: image quality — min width, contrast gate, crop box, deskew.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from "./runner";
|
||||
import {
|
||||
AI_IMAGE_LONG_EDGE_PX,
|
||||
AI_IMAGE_MIN_WIDTH_PX,
|
||||
computeReceiptResize,
|
||||
} from "../../src/lib/image/resize";
|
||||
import {
|
||||
contentBoundingBox,
|
||||
estimateSkewDegrees,
|
||||
needsContrastBoost,
|
||||
} from "../../src/lib/image/enhance";
|
||||
import { processReceiptImage } from "../../src/lib/image/processor";
|
||||
|
||||
describe("Sprint C — Mindestbreite", () => {
|
||||
test("Querformat bleibt am 1536-Long-Edge", () => {
|
||||
const out = computeReceiptResize(2000, 1400);
|
||||
expect(out.width).toBe(1536);
|
||||
expect(out.height).toBe(1075);
|
||||
});
|
||||
|
||||
test("quadratische kleine Fotos werden nicht hochskaliert", () => {
|
||||
expect(computeReceiptResize(400, 400)).toEqual({ width: 400, height: 400 });
|
||||
});
|
||||
|
||||
test("hoher Thermobon wird breiter als bei reinem Long-Edge-Cap", () => {
|
||||
const srcW = 800;
|
||||
const srcH = 3500;
|
||||
const longEdgeOnlyW = Math.round(srcW * (AI_IMAGE_LONG_EDGE_PX / srcH));
|
||||
const out = computeReceiptResize(srcW, srcH);
|
||||
expect(longEdgeOnlyW).toBeLessThan(400);
|
||||
expect(out.width).toBeGreaterThan(longEdgeOnlyW);
|
||||
expect(out.width).toBeGreaterThanOrEqual(650);
|
||||
expect(Math.max(out.width, out.height)).toBeLessThanOrEqual(4096);
|
||||
});
|
||||
|
||||
test("Min-Width-Konstante ist 1100", () => {
|
||||
expect(AI_IMAGE_MIN_WIDTH_PX).toBe(1100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sprint C — Kontrast", () => {
|
||||
test("verwaschenes Thermobild (niedrige Stdev) bekommt Boost", () => {
|
||||
expect(
|
||||
needsContrastBoost({
|
||||
channels: [{ mean: 170, stdev: 12, min: 140, max: 200 }],
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("farbstarkes Foto bekommt keinen Boost", () => {
|
||||
expect(
|
||||
needsContrastBoost({
|
||||
channels: [
|
||||
{ mean: 90, stdev: 55, min: 10, max: 240 },
|
||||
{ mean: 100, stdev: 60, min: 8, max: 250 },
|
||||
{ mean: 80, stdev: 50, min: 5, max: 230 },
|
||||
],
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sprint C — Crop", () => {
|
||||
test("dunkles Rechteck auf weißem Grund wird als Inhalt erkannt", () => {
|
||||
const w = 80;
|
||||
const h = 80;
|
||||
const data = new Uint8Array(w * h).fill(250);
|
||||
for (let y = 18; y < 62; y++) {
|
||||
for (let x = 16; x < 64; x++) {
|
||||
data[y * w + x] = 20;
|
||||
}
|
||||
}
|
||||
const box = contentBoundingBox(data, w, h);
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.left).toBeLessThanOrEqual(16);
|
||||
expect(box!.top).toBeLessThanOrEqual(18);
|
||||
expect(box!.left + box!.width).toBeGreaterThanOrEqual(64);
|
||||
expect(box!.top + box!.height).toBeGreaterThanOrEqual(62);
|
||||
});
|
||||
|
||||
test("einfarbiges Bild wird nicht beschnitten", () => {
|
||||
const data = new Uint8Array(40 * 40).fill(200);
|
||||
expect(contentBoundingBox(data, 40, 40)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sprint C — Deskew", () => {
|
||||
test("waagerechte Textzeilen → Winkel ~ 0", () => {
|
||||
const w = 64;
|
||||
const h = 80;
|
||||
const data = new Uint8Array(w * h);
|
||||
for (let y = 0; y < h; y++) {
|
||||
const dark = y % 8 < 3;
|
||||
data.fill(dark ? 15 : 230, y * w, y * w + w);
|
||||
}
|
||||
expect(Math.abs(estimateSkewDegrees(data, w, h))).toBeLessThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sprint C — Pipeline", () => {
|
||||
test("1×1-PNG überlebt Deskew/Crop/Resize", async () => {
|
||||
const png = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"base64"
|
||||
);
|
||||
const result = await processReceiptImage(png, "image/png");
|
||||
expect(result.mimeType).toBe("image/jpeg");
|
||||
expect(result.width).toBeGreaterThan(0);
|
||||
expect(result.height).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user