Day 1 (monorepo + Node layout engine): - Turborepo + pnpm workspaces with apps/web, apps/render-worker, and packages for types, layout-engine, prompts, api-lib. - @banner-studio/types: BannerSpec contract, every layer kind, ResolvedLayer, zod schemas mirroring each interface. - @banner-studio/layout-engine: Dropflow WASM wrapper, text measurement, shrink-to-fit, push_siblings, resolveLayout. Snapshot-tested. Day 2 (browser parity + AI pipeline): - Layout engine ./browser subpath: same resolveLayout in the browser via Dropflow WASM build. Quarantined wasm-locator import (dropflow 0.5.1 exports gap). - Cross-group push_siblings bug fix: deltas now thread through group recursion via a shared accumulator; regression test added. - DEMO_TEMPLATE_300x250 promoted to packages/layout-engine/src/templates/. - @banner-studio/prompts: versioned extract + generate prompts with zod-defined tool schemas (claude-sonnet-4-6, forced tool-use). - @banner-studio/api-lib: CSV feed loader, extract/generate/route-node/ assemble agents, orchestrator returning fully-resolved BannerSpec. Generate agent retries on character-limit overflow. - apps/web (Next.js 14 App Router): /api/generate route, /parity diff page, promise-singleton browser engine init. - feeds/demo.csv with five hand-authored rows of varied length. - SLICE_DEVIATIONS.md documents the five intentional gaps from ARCHITECTURE.md with V1 reversal paths. Verified end-to-end: POST /api/generate against the live Claude API returns three resolved BannerSpecs and two honestly-skipped rows (overflow after two attempts). 26 unit + integration tests passing.
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
import { beforeAll, describe, expect, it } from 'vitest';
|
|
import { measureText } from '../src/index.js';
|
|
import { ensureEngineReady } from './_setup.js';
|
|
import type { TypographySpec } from '@banner-studio/types';
|
|
|
|
const typography: TypographySpec = {
|
|
font_family: 'Inter',
|
|
font_size: 16,
|
|
font_weight: 400,
|
|
line_height: 1.3,
|
|
color: '#000000',
|
|
text_align: 'left'
|
|
};
|
|
|
|
describe('dropflow-wrapper / measureText', () => {
|
|
beforeAll(async () => {
|
|
await ensureEngineReady();
|
|
});
|
|
|
|
it('returns positive width and height for a short single-line string', () => {
|
|
const m = measureText({ text: 'Hello world', typography, max_width: 300 });
|
|
expect(m.width).toBeGreaterThan(0);
|
|
expect(m.height).toBeGreaterThan(0);
|
|
expect(m.line_count).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('wraps text to multiple lines when max_width is small', () => {
|
|
const long =
|
|
'The quick brown fox jumps over the lazy dog and keeps on running into the distance.';
|
|
const wide = measureText({ text: long, typography, max_width: 500 });
|
|
const narrow = measureText({ text: long, typography, max_width: 100 });
|
|
expect(narrow.line_count).toBeGreaterThan(wide.line_count);
|
|
expect(narrow.height).toBeGreaterThan(wide.height);
|
|
});
|
|
|
|
it('measures larger text as taller', () => {
|
|
const small = measureText({ text: 'Test', typography, max_width: 300 });
|
|
const big = measureText({
|
|
text: 'Test',
|
|
typography: { ...typography, font_size: 32 },
|
|
max_width: 300
|
|
});
|
|
expect(big.height).toBeGreaterThan(small.height);
|
|
});
|
|
});
|