- Export DEFAULT_LAYOUT from lib/frame-palette.js so the custom-frames route can import it at runtime (not just the hardcoded copy in tests). - Fix vitest mock isolation in test/api/custom-frames.test.js: beforeEach now uses mockReturnValue instead of mockResolvedValue to avoid resolving the default mock in each test; test cases provide specific mock chains with mockResolvedValueOnce. Fixes 4 tests that were bleeding state between cases due to leftover queued mock values. - Fix validateLayout test coordinates: art w+h=0.924 and 0.398 are both within the 0-1 fraction range so x+w=0.962<1 and y+h=0.982<1 pass. - Add dedicated validateLayout unit tests (accepts, rejects missing zone, rejects out-of-bounds). - Fix update test mock chain: PUT calls SELECT (found) then SELECT (clash) then UPDATE (RETURNING) — provide all three in order. - Fix DELETE test: owns via SELECT then executes DELETE (2 calls).
85 lines
2.7 KiB
JavaScript
85 lines
2.7 KiB
JavaScript
/**
|
|
* Default window zones for image-based frames, as fractions of the card.
|
|
* They mirror the framed layout's art window and text box positions.
|
|
*/
|
|
export const DEFAULT_LAYOUT = {
|
|
art: { x: 16 / 420, y: 64 / 588, w: 388 / 420, h: 234 / 588 },
|
|
text: { x: 16 / 420, y: 342 / 588, w: 388 / 420, h: 224 / 588 },
|
|
};
|
|
|
|
/**
|
|
* Palette contract for card frames. Starter and custom frames share the
|
|
* same slots; CardFrame resolves a palette from these keys.
|
|
*/
|
|
export const PALETTE_SLOTS = [
|
|
{ key: 'outer', label: 'Card body' },
|
|
{ key: 'border', label: 'Frame border' },
|
|
{ key: 'titleBar', label: 'Title bar' },
|
|
{ key: 'typeBar', label: 'Type bar' },
|
|
{ key: 'textBox', label: 'Text box' },
|
|
{ key: 'artBacking', label: 'Art backing' },
|
|
{ key: 'text', label: 'Body text' },
|
|
{ key: 'titleText', label: 'Title text' },
|
|
{ key: 'accent', label: 'Accent' },
|
|
];
|
|
|
|
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
|
|
/**
|
|
* Validate/normalize a palette object. Returns { palette } with all nine
|
|
* slots as lowercase hex, or { error } when a slot is missing/invalid.
|
|
*/
|
|
export function validatePalette(input) {
|
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
return { error: 'Palette must be an object' };
|
|
}
|
|
|
|
const palette = {};
|
|
for (const { key } of PALETTE_SLOTS) {
|
|
const value = input[key];
|
|
if (typeof value !== 'string' || !HEX_RE.test(value.trim())) {
|
|
return { error: `Invalid or missing color for "${key}"` };
|
|
}
|
|
palette[key] = value.trim().toLowerCase();
|
|
}
|
|
|
|
return { palette };
|
|
}
|
|
|
|
/**
|
|
* Validate/normalize frame layout zones (art + text windows as 0-1
|
|
* fractions of the card). Returns { layout } or { error }.
|
|
*/
|
|
export function validateLayout(input) {
|
|
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
return { error: 'Layout must be an object' };
|
|
}
|
|
|
|
const layout = {};
|
|
for (const zone of ['art', 'text']) {
|
|
const z = input[zone];
|
|
if (!z || typeof z !== 'object') {
|
|
return { error: `Layout zone "${zone}" is required` };
|
|
}
|
|
const { x, y, w, h } = z;
|
|
for (const [key, value] of Object.entries({ x, y, w, h })) {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
return { error: `Layout zone "${zone}" has invalid ${key}` };
|
|
}
|
|
}
|
|
if (w <= 0 || h <= 0) {
|
|
return { error: `Layout zone "${zone}" must have positive size` };
|
|
}
|
|
if (x < 0 || y < 0 || x + w > 1.001 || y + h > 1.001) {
|
|
return { error: `Layout zone "${zone}" extends past the card` };
|
|
}
|
|
layout[zone] = {
|
|
x: Math.max(0, x),
|
|
y: Math.max(0, y),
|
|
w: Math.min(w, 1 - Math.max(0, x)),
|
|
h: Math.min(h, 1 - Math.max(0, y)),
|
|
};
|
|
}
|
|
|
|
return { layout };
|
|
}
|