diff --git a/render/__tests__/build-figur.test.mjs b/render/__tests__/build-figur.test.mjs
new file mode 100644
index 0000000..e814943
--- /dev/null
+++ b/render/__tests__/build-figur.test.mjs
@@ -0,0 +1,284 @@
+import { describe, test } from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execFileSync } from 'node:child_process';
+import {
+ TARGETS,
+ DEFAULT_TOKENS,
+ deriveDimensions,
+ loadBrandTokens,
+ tokensToCss,
+ wrapSvg,
+ injectTokens,
+ parseSvgMeta,
+ validateFigure,
+ resolveChrome,
+ parseArgs,
+ renderFigure,
+} from '../build-figur.mjs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const FIXTURE_SVG = path.join(__dirname, 'fixtures', 'demo-figur.svg');
+
+function tmpdir() {
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'build-figur-test-'));
+}
+
+// Leser bredde/høyde fra PNG-IHDR (byte 16–23, big-endian).
+function pngDimensions(file) {
+ const buf = fs.readFileSync(file);
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
+}
+
+describe('TARGETS + deriveDimensions', () => {
+ test('carousel er 1080×1350 (fast)', () => {
+ assert.equal(TARGETS.carousel.width, 1080);
+ assert.equal(TARGETS.carousel.height, 1350);
+ });
+
+ test('article er 1200 bred med innholdsstyrt høyde', () => {
+ assert.equal(TARGETS.article.width, 1200);
+ assert.equal(TARGETS.article.height, null);
+ });
+
+ test('single er kvadratisk 1200×1200', () => {
+ assert.equal(TARGETS.single.width, 1200);
+ assert.equal(TARGETS.single.height, 1200);
+ });
+
+ test('article + aspekt 16:9 gir avledet høyde 675', () => {
+ const d = deriveDimensions('article', 1200 / 675, {});
+ assert.equal(d.width, 1200);
+ assert.equal(d.height, 675);
+ assert.equal(d.warnings.length, 0);
+ });
+
+ test('article uten aspekt gir fallback-høyde med warning', () => {
+ const d = deriveDimensions('article', null, {});
+ assert.equal(d.width, 1200);
+ assert.ok(d.height > 0);
+ assert.ok(d.warnings.some((w) => /høyde/i.test(w)));
+ });
+
+ test('--width/--height-overstyringer vinner over målet', () => {
+ const d = deriveDimensions('carousel', null, { width: 800, height: 600 });
+ assert.equal(d.width, 800);
+ assert.equal(d.height, 600);
+ });
+
+ test('ukjent mål kaster med gyldige mål i meldingen', () => {
+ assert.throws(() => deriveDimensions('poster', null, {}), /article/);
+ });
+});
+
+describe('loadBrandTokens — token-seam (brukerdata, aldri hardkodet)', () => {
+ test('uten brand-tokens.json: nøytrale defaults, source=default', () => {
+ const dir = tmpdir();
+ const r = loadBrandTokens(dir);
+ assert.equal(r.source, 'default');
+ assert.deepEqual(r.tokens, DEFAULT_TOKENS);
+ assert.equal(r.warnings.length, 0);
+ });
+
+ test('gyldig fil merges over defaults, source=user', () => {
+ const dir = tmpdir();
+ fs.mkdirSync(path.join(dir, 'profile'), { recursive: true });
+ fs.writeFileSync(
+ path.join(dir, 'profile', 'brand-tokens.json'),
+ JSON.stringify({ accent: '#123456' }),
+ 'utf8'
+ );
+ const r = loadBrandTokens(dir);
+ assert.equal(r.source, 'user');
+ assert.equal(r.tokens.accent, '#123456');
+ // umerkede felter beholder defaults
+ assert.equal(r.tokens.background, DEFAULT_TOKENS.background);
+ });
+
+ test('ugyldig JSON: defaults + warning, kaster aldri', () => {
+ const dir = tmpdir();
+ fs.mkdirSync(path.join(dir, 'profile'), { recursive: true });
+ fs.writeFileSync(path.join(dir, 'profile', 'brand-tokens.json'), '{not json', 'utf8');
+ let r;
+ assert.doesNotThrow(() => { r = loadBrandTokens(dir); });
+ assert.equal(r.source, 'default');
+ assert.ok(r.warnings.some((w) => /brand-tokens\.json/.test(w)));
+ });
+});
+
+describe('tokensToCss + wrapSvg + injectTokens — CSS-injeksjon', () => {
+ test('tokensToCss produserer --figur-variabler for alle tokens', () => {
+ const css = tokensToCss(DEFAULT_TOKENS);
+ assert.match(css, /:root\{/);
+ for (const key of Object.keys(DEFAULT_TOKENS)) {
+ assert.ok(css.includes(`--figur-${key}`), `mangler --figur-${key}`);
+ }
+ });
+
+ test('egendefinerte tokenverdier reflekteres i CSS-en', () => {
+ const css = tokensToCss({ ...DEFAULT_TOKENS, accent: '#ABCDEF' });
+ assert.match(css, /--figur-accent:\s*#ABCDEF/);
+ });
+
+ test('wrapSvg bygger komplett HTML med svg + tokens + mål-dimensjoner', () => {
+ const html = wrapSvg('', { width: 1080, height: 1350 }, DEFAULT_TOKENS);
+ assert.match(html, //i);
+ assert.match(html, /