Files
SlabUploader/frontend/src/png/inventory.test.ts
T
grokbot_ty f20d84c543 test(capture): expect 800px inventory PNG floor
Ty UAT rejected phone photos under the 1600px bar.
Lock the mid-band case (shorter side 1000) and floor
geometry at 800×1067 / 0.8×800=640 before the code change.
2026-09-14 23:40:01 +00:00

121 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { composeInventoryFrame } from './inventory.ts';
import type { Mask, Source } from '../capture/capture.types';
const makePhoto = (w: number, h: number, paint: (x: number, y: number) => [number, number, number, number] = () => [255, 255, 255, 255]) => {
const rgba = new Uint8Array(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const [r, g, b, a] = paint(x, y);
const i = (y * w + x) * 4;
rgba[i] = r; rgba[i + 1] = g; rgba[i + 2] = b; rgba[i + 3] = a;
}
}
return { width: w, height: h, rgba };
};
const maskFor = (bitset: Uint8Array, slabBounds: Mask['slabBounds']): Pick<Mask, 'bitset' | 'slabBounds'> =>
({ bitset, slabBounds });
test('alpha contract: removed pixels transparent, slab pixels keep RGB at alpha 255', () => {
const W = 8, H = 8;
const bitset = new Uint8Array(W * H);
for (let y = 1; y < 5; y++) for (let x = 1; x < 5; x++) bitset[y * W + x] = 1;
bitset[2 * W + 2] = 0;
bitset[3 * W + 3] = 0;
const SLAB = [200, 40, 20, 255] as const;
const SHEET = [0, 255, 0, 255] as const;
const src = makePhoto(W, H, (x, y) => (bitset[y * W + x] === 1 ? [...SLAB] : [...SHEET]));
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: W, heightPx: H };
const out = composeInventoryFrame(src, maskFor(bitset, { x: 1, y: 1, w: 4, h: 4 }), source);
assert.equal(out.sourceBelowFloor, true);
assert.ok(out.width < 800, 'undersized source stays small');
let opaque = 0;
let transparent = 0;
for (let i = 0; i < out.rgba.length; i += 4) {
const a = out.rgba[i + 3];
if (a === 0) {
transparent++;
assert.equal(out.rgba[i], 0);
assert.equal(out.rgba[i + 1], 0);
assert.equal(out.rgba[i + 2], 0);
continue;
}
opaque++;
assert.equal(a, 255);
assert.deepEqual(
[out.rgba[i], out.rgba[i + 1], out.rgba[i + 2]],
[SLAB[0], SLAB[1], SLAB[2]],
'opaque pixel must be slab RGB, never sheet green',
);
}
assert.ok(opaque > 0, 'slab pixels are opaque');
assert.ok(transparent > 0, 'frame has transparent pixels');
});
test('3:4 frame at the floor: 2000×1800 slab → 800×1067', () => {
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 2000, heightPx: 1800 };
const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
const out = composeInventoryFrame(
makePhoto(source.widthPx, source.heightPx),
maskFor(bitset, { x: 0, y: 0, w: 2000, h: 1800 }),
source,
);
assert.equal(out.width, 800);
assert.equal(out.height, 1067);
assert.equal(out.sourceBelowFloor, false);
});
test('mid-band: slab shorter side 1000 (between 800 and 1600) → at floor, not belowFloor', () => {
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 1200, heightPx: 1000 };
const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
const out = composeInventoryFrame(
makePhoto(source.widthPx, source.heightPx),
maskFor(bitset, { x: 0, y: 0, w: 1200, h: 1000 }),
source,
);
assert.equal(out.sourceBelowFloor, false);
assert.equal(out.width, 800);
});
test('80% fill: limiting dimension maps to 0.8 × frame side', () => {
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 2200, heightPx: 2000 };
const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
const out = composeInventoryFrame(
makePhoto(source.widthPx, source.heightPx),
maskFor(bitset, { x: 100, y: 100, w: 2000, h: 1800 }),
source,
);
let minx = out.width, maxx = -1, miny = out.height, maxy = -1;
for (let y = 0; y < out.height; y++) {
for (let x = 0; x < out.width; x++) {
if (out.rgba[(y * out.width + x) * 4 + 3] !== 0) {
if (x < minx) minx = x;
if (x > maxx) maxx = x;
if (y < miny) miny = y;
if (y > maxy) maxy = y;
}
}
}
const xs = [minx, maxx], ys = [miny, maxy];
const wExtent = Math.max(...xs) - Math.min(...xs) + 1;
const hExtent = Math.max(...ys) - Math.min(...ys) + 1;
assert.ok(Math.abs(wExtent - 640) <= 2, `width extent ${wExtent} ≈ 640 (0.8×800)`);
assert.ok(Math.abs(hExtent - 576) <= 2, `height extent ${hExtent} ≈ 576 (1800×0.32)`);
});
test('belowFloor: small slab → sourceBelowFloor, outW capped at floor (no upscale)', () => {
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 900, heightPx: 700 };
const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
const out = composeInventoryFrame(
makePhoto(source.widthPx, source.heightPx),
maskFor(bitset, { x: 0, y: 0, w: 900, h: 700 }),
source,
);
assert.equal(out.sourceBelowFloor, true);
assert.ok(out.width <= 800, `outW ${out.width} must not exceed the floor`);
});