mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fix 2 failing unit tests (landing hero text mismatch) and broken coverage tooling (brace-expansion v5 override breaking minimatch). Add ~1,097 new test cases via 14-agent parallel expansion: - Unit: +290 tests (AI bridge, image-engine, stores, API helpers) - Integration: +504 tests (all tools, cross-format matrix, adversarial) - E2E: +363 tests (navigation, tool UI, batch/pipeline, settings, visual regression, accessibility, performance, cross-browser) Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
244 lines
7.4 KiB
TypeScript
244 lines
7.4 KiB
TypeScript
/**
|
|
* Expanded metadata tests covering:
|
|
* - sanitizeValue edge cases: deeply nested, mixed types, empty arrays/objects
|
|
* - parseGps edge cases: NaN altitude, non-numeric altitude, missing ref
|
|
* - parseXmp edge cases: multiple overlapping keys, multiline XML
|
|
* - parseExif: corrupt EXIF buffer (parse failure catch branch)
|
|
*/
|
|
import { parseGps, parseXmp, sanitizeValue } from "@snapotter/image-engine";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
describe("sanitizeValue edge cases", () => {
|
|
it("handles empty array", () => {
|
|
expect(sanitizeValue([])).toEqual([]);
|
|
});
|
|
|
|
it("handles empty object", () => {
|
|
expect(sanitizeValue({})).toEqual({});
|
|
});
|
|
|
|
it("handles deeply nested structure", () => {
|
|
const input = {
|
|
a: {
|
|
b: {
|
|
c: {
|
|
d: new Date("2025-12-25T00:00:00Z"),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const result = sanitizeValue(input) as Record<string, unknown>;
|
|
expect((result as any).a.b.c.d).toBe("2025-12-25T00:00:00.000Z");
|
|
});
|
|
|
|
it("handles array with mixed types including Buffers and Dates", () => {
|
|
const input = [
|
|
42,
|
|
"hello",
|
|
new Date("2024-01-01T00:00:00Z"),
|
|
Buffer.from([10, 20]),
|
|
null,
|
|
undefined,
|
|
true,
|
|
];
|
|
const result = sanitizeValue(input);
|
|
expect(result).toEqual([
|
|
42,
|
|
"hello",
|
|
"2024-01-01T00:00:00.000Z",
|
|
[10, 20],
|
|
null,
|
|
undefined,
|
|
true,
|
|
]);
|
|
});
|
|
|
|
it("handles object with Buffer value at exactly 256 bytes", () => {
|
|
const buf = Buffer.alloc(256, 0xaa);
|
|
const result = sanitizeValue({ data: buf }) as Record<string, unknown>;
|
|
// 256 is not > 256, so it should be an array
|
|
expect(Array.isArray(result.data)).toBe(true);
|
|
expect((result.data as number[]).length).toBe(256);
|
|
});
|
|
|
|
it("handles object with Buffer value at 257 bytes", () => {
|
|
const buf = Buffer.alloc(257, 0xbb);
|
|
const result = sanitizeValue({ data: buf }) as Record<string, unknown>;
|
|
expect(result.data).toBe("<binary 257 bytes>");
|
|
});
|
|
|
|
it("handles string values without transformation", () => {
|
|
expect(sanitizeValue("")).toBe("");
|
|
expect(sanitizeValue("test string")).toBe("test string");
|
|
});
|
|
|
|
it("handles NaN and Infinity numbers", () => {
|
|
expect(sanitizeValue(NaN)).toBeNaN();
|
|
expect(sanitizeValue(Infinity)).toBe(Infinity);
|
|
expect(sanitizeValue(-Infinity)).toBe(-Infinity);
|
|
});
|
|
|
|
it("handles zero-length buffer", () => {
|
|
const buf = Buffer.alloc(0);
|
|
expect(sanitizeValue(buf)).toEqual([]);
|
|
});
|
|
|
|
it("handles nested array of objects with dates", () => {
|
|
const input = [
|
|
{ ts: new Date("2025-01-01T00:00:00Z") },
|
|
{ ts: new Date("2025-06-01T00:00:00Z") },
|
|
];
|
|
const result = sanitizeValue(input) as Array<Record<string, unknown>>;
|
|
expect(result[0].ts).toBe("2025-01-01T00:00:00.000Z");
|
|
expect(result[1].ts).toBe("2025-06-01T00:00:00.000Z");
|
|
});
|
|
});
|
|
|
|
describe("parseGps edge cases", () => {
|
|
it("handles NaN altitude", () => {
|
|
const result = parseGps({
|
|
GPSAltitude: NaN,
|
|
GPSAltitudeRef: 0,
|
|
});
|
|
expect(result.altitude).toBeNull();
|
|
});
|
|
|
|
it("handles non-numeric altitude (string)", () => {
|
|
const result = parseGps({
|
|
GPSAltitude: "100" as unknown as number,
|
|
});
|
|
// typeof "100" is not "number", so altitude should be null
|
|
expect(result.altitude).toBeNull();
|
|
});
|
|
|
|
it("handles undefined altitude ref (positive altitude)", () => {
|
|
const result = parseGps({
|
|
GPSAltitude: 50,
|
|
});
|
|
expect(result.altitude).toBe(50);
|
|
});
|
|
|
|
it("handles altitude ref = 0 (above sea level)", () => {
|
|
const result = parseGps({
|
|
GPSAltitude: 100,
|
|
GPSAltitudeRef: 0,
|
|
});
|
|
expect(result.altitude).toBe(100);
|
|
});
|
|
|
|
it("handles altitude ref = 1 (below sea level)", () => {
|
|
const result = parseGps({
|
|
GPSAltitude: 100,
|
|
GPSAltitudeRef: 1,
|
|
});
|
|
expect(result.altitude).toBe(-100);
|
|
});
|
|
|
|
it("handles lat without ref (defaults to positive)", () => {
|
|
const result = parseGps({
|
|
GPSLatitude: [10, 0, 0],
|
|
});
|
|
expect(result.latitude).toBeCloseTo(10, 3);
|
|
});
|
|
|
|
it("handles lon without ref (defaults to positive)", () => {
|
|
const result = parseGps({
|
|
GPSLongitude: [20, 30, 0],
|
|
});
|
|
expect(result.longitude).toBeCloseTo(20.5, 3);
|
|
});
|
|
|
|
it("ignores latitude with wrong length (4 elements)", () => {
|
|
const result = parseGps({
|
|
GPSLatitude: [10, 20, 30, 40],
|
|
GPSLatitudeRef: "N",
|
|
});
|
|
expect(result.latitude).toBeNull();
|
|
});
|
|
|
|
it("ignores latitude with wrong length (1 element)", () => {
|
|
const result = parseGps({
|
|
GPSLatitude: [10],
|
|
GPSLatitudeRef: "N",
|
|
});
|
|
expect(result.latitude).toBeNull();
|
|
});
|
|
|
|
it("handles zero coordinates (prime meridian / equator)", () => {
|
|
const result = parseGps({
|
|
GPSLatitude: [0, 0, 0],
|
|
GPSLatitudeRef: "N",
|
|
GPSLongitude: [0, 0, 0],
|
|
GPSLongitudeRef: "E",
|
|
});
|
|
expect(result.latitude).toBe(0);
|
|
expect(result.longitude).toBe(0);
|
|
});
|
|
|
|
it("handles maximum coordinate values", () => {
|
|
const result = parseGps({
|
|
GPSLatitude: [90, 0, 0],
|
|
GPSLatitudeRef: "N",
|
|
GPSLongitude: [180, 0, 0],
|
|
GPSLongitudeRef: "E",
|
|
});
|
|
expect(result.latitude).toBe(90);
|
|
expect(result.longitude).toBe(180);
|
|
});
|
|
});
|
|
|
|
describe("parseXmp edge cases", () => {
|
|
it("handles multiple attributes on same element", () => {
|
|
const xml = Buffer.from(
|
|
'<rdf:Description xmp:Rating="5" xmp:Label="Select" dc:title="Photo 1" />',
|
|
);
|
|
const result = parseXmp(xml);
|
|
expect(result["xmp:Rating"]).toBe("5");
|
|
expect(result["xmp:Label"]).toBe("Select");
|
|
expect(result["dc:title"]).toBe("Photo 1");
|
|
});
|
|
|
|
it("handles attributes with special characters in values", () => {
|
|
const xml = Buffer.from('<rdf:Description dc:creator="John & Jane" />');
|
|
const result = parseXmp(xml);
|
|
// The regex matches until the closing quote, so "John & Jane" is captured
|
|
expect(result["dc:creator"]).toBe("John & Jane");
|
|
});
|
|
|
|
it("ignores attributes without namespace prefix", () => {
|
|
// The regex requires "word:word" pattern, so single-word attributes are skipped
|
|
const xml = Buffer.from('<element standalone="true" ns:valid="yes" />');
|
|
const result = parseXmp(xml);
|
|
expect(result.standalone).toBeUndefined();
|
|
expect(result["ns:valid"]).toBe("yes");
|
|
});
|
|
|
|
it("handles XML with multiple rdf:Description elements", () => {
|
|
const xml = Buffer.from(
|
|
'<rdf:Description dc:title="First" />' +
|
|
'<rdf:Description exif:DateTimeOriginal="2025-01-01" />',
|
|
);
|
|
const result = parseXmp(xml);
|
|
expect(result["dc:title"]).toBe("First");
|
|
expect(result["exif:DateTimeOriginal"]).toBe("2025-01-01");
|
|
});
|
|
|
|
it("handles large XMP buffer without error", () => {
|
|
// Generate a large but parseable XMP
|
|
const attrs = Array.from({ length: 100 }, (_, i) => `ns:key${i}="val${i}"`).join(" ");
|
|
const xml = Buffer.from(`<rdf:Description ${attrs} />`);
|
|
const result = parseXmp(xml);
|
|
expect(Object.keys(result).length).toBe(100);
|
|
expect(result["ns:key0"]).toBe("val0");
|
|
expect(result["ns:key99"]).toBe("val99");
|
|
});
|
|
|
|
it("handles empty attributes as empty strings", () => {
|
|
const xml = Buffer.from('<rdf:Description dc:title="" />');
|
|
// The regex /(\w+:\w+)="([^"]+)"/ requires at least one char between quotes
|
|
// So empty values are NOT matched -- this is correct behavior
|
|
const result = parseXmp(xml);
|
|
expect(result["dc:title"]).toBeUndefined();
|
|
});
|
|
});
|