Files
site-builder/craft/src/utils/apply-ai-response.test.ts
Josh Knapp cf3457aa15 sitesmith: apply-ai-response utility (replace + patch + ask) + PageContext helpers
Add apply-ai-response.ts with serializeTreeForCraft, buildNodeTree, findNodeIdByAiNodeId,
and useApplyAiResponse hook covering replace (site/page/section), patch (5 ops), and ask.
Extend PageContext with replaceAllPages, replaceCurrentPage, setHeader, setFooter helpers
that mirror the existing actions.deserialize/loadState pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:20:51 -07:00

91 lines
2.8 KiB
TypeScript

import { describe, test, expect } from 'vitest';
import { serializeTreeForCraft, __test } from './apply-ai-response';
describe('serializeTreeForCraft', () => {
test('flattens nested tree', () => {
const tree = {
type: { resolvedName: 'Section' },
props: { aiName: 'Hero', node_id: 'ai-hero-1' },
nodes: [
{
type: { resolvedName: 'Heading' },
props: { aiName: 'Title', node_id: 'ai-h-1', text: 'Welcome' },
nodes: [],
},
],
};
const out = serializeTreeForCraft(tree);
expect(out.rootNodeId).toBe('ai-hero-1');
expect((out.nodes['ai-hero-1'] as any).nodes).toEqual(['ai-h-1']);
expect((out.nodes['ai-h-1'] as any).parent).toBe('ROOT');
});
test('auto-generates ids when node_id is missing', () => {
const tree = { type: { resolvedName: 'Heading' }, props: {}, nodes: [] };
const out = serializeTreeForCraft(tree);
expect(typeof out.rootNodeId).toBe('string');
expect(out.nodes[out.rootNodeId]).toBeDefined();
});
test('sets isCanvas true for layout components', () => {
const tree = {
type: { resolvedName: 'Container' },
props: { node_id: 'c1' },
nodes: [],
};
const out = serializeTreeForCraft(tree);
expect((out.nodes['ROOT'] as any).isCanvas).toBe(true);
});
test('sets isCanvas false for leaf components', () => {
const tree = {
type: { resolvedName: 'Heading' },
props: { node_id: 'h1' },
nodes: [],
};
const out = serializeTreeForCraft(tree);
expect((out.nodes['ROOT'] as any).isCanvas).toBe(false);
});
test('aliases root node to ROOT key', () => {
const tree = {
type: { resolvedName: 'Section' },
props: { node_id: 'ai-section-1' },
nodes: [],
};
const out = serializeTreeForCraft(tree);
expect(out.nodes['ROOT']).toBeDefined();
expect((out.nodes['ROOT'] as any).parent).toBeNull();
});
});
describe('findNodeIdByAiNodeId', () => {
const query = {
getNodes: () => ({
'craft-id-1': { data: { props: { node_id: 'ai-hero-1' } } },
'craft-id-2': { data: { props: { node_id: 'ai-cta-1' } } },
}),
};
test('returns craft id for matching node_id prop', () => {
expect(__test.findNodeIdByAiNodeId(query, 'ai-hero-1')).toBe('craft-id-1');
});
test('returns craft id for second entry', () => {
expect(__test.findNodeIdByAiNodeId(query, 'ai-cta-1')).toBe('craft-id-2');
});
test('falls back to raw id match', () => {
const q = {
getNodes: () => ({
'exact-id': { data: { props: {} } },
}),
};
expect(__test.findNodeIdByAiNodeId(q, 'exact-id')).toBe('exact-id');
});
test('returns null when not found', () => {
expect(__test.findNodeIdByAiNodeId({ getNodes: () => ({}) }, 'missing')).toBe(null);
});
});