test(ai): exercise extractQueryForNode + findParentSemanticName
Why: 810c2f7a added the parent-walk query mining for heuristic image-
area frames but landed without unit coverage of the two new helpers.
Without it, a future edit to GENERIC_PLACEHOLDER_NAMES or the layout-
word filter could silently regress query quality (e.g. start emitting
"Wrapper" or "Section" as queries) and the food-app card photos would
go back to looking generic.
What: export the two helpers and add 5 it() cases covering:
- explicit imageSearchQuery wins over name
- generic literal "Image" + parent "Bella Italia" → returns "Bella Italia"
- skip layout words ("Card Wrapper") in the parent walk; accept the
next semantic ancestor ("Margherita Pizza")
- 4-hop layout-only chain returns null (maxHops bound)
- fall back to non-generic-but-image-themed name when parent walk
yields nothing ("My Custom Photo")
31 / 31 tests in image-search-pipeline.test.ts pass (was 26; +5).
1103 / 1103 AI service tests pass (was 1098; +5).
This commit is contained in:
parent
f072db8559
commit
7fedfff954
|
|
@ -5,6 +5,8 @@ import {
|
|||
isUnfilledImagePlaceholderFrame,
|
||||
isImageAreaFrameByHeuristic,
|
||||
collectImageSearchTargets,
|
||||
extractQueryForNode,
|
||||
findParentSemanticName,
|
||||
} from '../image-search-pipeline';
|
||||
import { useDocumentStore } from '@/stores/document-store';
|
||||
import type { PenNode } from '@/types/pen';
|
||||
|
|
@ -399,3 +401,145 @@ describe('isImageAreaFrameByHeuristic', () => {
|
|||
expect(isImageAreaFrameByHeuristic(fill)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractQueryForNode + findParentSemanticName', () => {
|
||||
beforeEach(() => {
|
||||
useDocumentStore.setState({
|
||||
document: { children: [], variables: [], themes: [], pages: [] },
|
||||
isDirty: false,
|
||||
} as never);
|
||||
});
|
||||
|
||||
function loadTree(rootChildren: PenNode[]) {
|
||||
useDocumentStore.setState({
|
||||
document: {
|
||||
children: [],
|
||||
variables: [],
|
||||
themes: [],
|
||||
pages: [
|
||||
{
|
||||
id: 'page-1',
|
||||
name: 'Page 1',
|
||||
children: [
|
||||
{ id: 'root', type: 'frame', name: 'Page', children: rootChildren } as PenNode,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isDirty: false,
|
||||
} as never);
|
||||
}
|
||||
|
||||
it('extractQueryForNode prefers explicit imageSearchQuery over name', () => {
|
||||
const node = {
|
||||
id: 'i',
|
||||
type: 'frame',
|
||||
name: 'Image',
|
||||
imageSearchQuery: 'sushi platter',
|
||||
} as PenNode;
|
||||
expect(extractQueryForNode(node)).toBe('sushi platter');
|
||||
});
|
||||
|
||||
it('extractQueryForNode skips generic placeholder names and walks to parent', () => {
|
||||
loadTree([
|
||||
{
|
||||
id: 'card',
|
||||
type: 'frame',
|
||||
name: 'Bella Italia',
|
||||
children: [
|
||||
{
|
||||
id: 'image-area',
|
||||
type: 'frame',
|
||||
name: 'Image',
|
||||
width: 200,
|
||||
height: 140,
|
||||
fill: [{ type: 'solid', color: '#FCD34D' }],
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
]);
|
||||
const node = useDocumentStore.getState().getNodeById('image-area')!;
|
||||
expect(extractQueryForNode(node)).toBe('Bella Italia');
|
||||
});
|
||||
|
||||
it('findParentSemanticName skips layout words (Card / Wrapper / Container)', () => {
|
||||
loadTree([
|
||||
{
|
||||
id: 'wrap',
|
||||
type: 'frame',
|
||||
name: 'Card Wrapper',
|
||||
children: [
|
||||
{
|
||||
id: 'inner-card',
|
||||
type: 'frame',
|
||||
name: 'Margherita Pizza',
|
||||
children: [
|
||||
{
|
||||
id: 'image-area',
|
||||
type: 'frame',
|
||||
name: 'Photo',
|
||||
width: 200,
|
||||
height: 140,
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
]);
|
||||
// Should skip "Card Wrapper" (matches the layout-word filter) and
|
||||
// accept "Margherita Pizza" (semantic).
|
||||
expect(findParentSemanticName('image-area')).toBe('Margherita Pizza');
|
||||
});
|
||||
|
||||
it('findParentSemanticName returns null when no semantic parent within hops', () => {
|
||||
loadTree([
|
||||
{
|
||||
id: 'wrap1',
|
||||
type: 'frame',
|
||||
name: 'Wrapper',
|
||||
children: [
|
||||
{
|
||||
id: 'wrap2',
|
||||
type: 'frame',
|
||||
name: 'Container',
|
||||
children: [
|
||||
{
|
||||
id: 'wrap3',
|
||||
type: 'frame',
|
||||
name: 'Section',
|
||||
children: [
|
||||
{
|
||||
id: 'wrap4',
|
||||
type: 'frame',
|
||||
name: 'Frame',
|
||||
children: [
|
||||
{
|
||||
id: 'image-area',
|
||||
type: 'frame',
|
||||
name: 'Image',
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
],
|
||||
} as PenNode,
|
||||
]);
|
||||
// Default maxHops=3; all 3 nearest parents are layout words.
|
||||
expect(findParentSemanticName('image-area')).toBeNull();
|
||||
});
|
||||
|
||||
it('extractQueryForNode falls back to name when parent walk yields nothing', () => {
|
||||
const node = {
|
||||
id: 'orphan',
|
||||
type: 'frame',
|
||||
name: 'My Custom Photo',
|
||||
} as PenNode;
|
||||
// Not loaded into store — parent map is empty, but the name itself
|
||||
// is non-generic ("My Custom Photo" — has "Photo" but it's not the
|
||||
// bare generic literal in the GENERIC_PLACEHOLDER_NAMES set).
|
||||
expect(extractQueryForNode(node)).toBe('My Custom Photo');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ function isGenericPlaceholderName(name: string): boolean {
|
|||
* "Image" name the model gave the placeholder. Bounded to 3 hops so a
|
||||
* deep page bg doesn't end up as the query.
|
||||
*/
|
||||
function findParentSemanticName(nodeId: string, maxHops = 3): string | null {
|
||||
export function findParentSemanticName(nodeId: string, maxHops = 3): string | null {
|
||||
const { document: doc } = useDocumentStore.getState();
|
||||
// Build a parent map by walking the doc tree once. Cheap for typical
|
||||
// designs (< few hundred nodes) and avoids passing parent through
|
||||
|
|
@ -220,7 +220,7 @@ function findParentSemanticName(nodeId: string, maxHops = 3): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function extractQueryForNode(node: PenNode): string {
|
||||
export function extractQueryForNode(node: PenNode): string {
|
||||
const r = node as PenNode & {
|
||||
imageSearchQuery?: string;
|
||||
name?: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue