// @vitest-environment node
import { describe, it, expect } from 'vitest';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveComponent } from '../resolver';
import type { ParsedFigmaFile } from '../parser';
import type { FigmaCodexConfig } from '../config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const FIXTURES = join(__dirname, '../fixtures');
const DDS_ROOT = join(__dirname, '../../..');
const baseConfig: FigmaCodexConfig = {
include: [],
outputPath: 'dist/figma-codex.json',
packageName: '@discourser/design-system',
tsconfig: 'tsconfig.json',
};
describe('resolveComponent', () => {
describe('simple component', () => {
const parsed: ParsedFigmaFile = {
filePath: join(FIXTURES, 'SimpleComponent.figma.tsx'),
importStyle: 'named',
componentName: 'SimpleComponent',
importSource: './SimpleComponent',
figmaUrl: 'https://www.figma.com/design/ABC123/TestFile?node-id=1-2',
figmaFileKey: 'ABC123',
figmaNodeId: '1:2',
example: '',
propsMapping: {},
};
it('classifies as simple', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.type).toBe('simple');
});
it('extracts named exports from source', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.imports.namedExports).toContain('SimpleComponent');
});
it('extracts prop definitions', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.props.length).toBeGreaterThan(0);
const labelProp = entry.props.find((p) => p.name === 'label');
expect(labelProp).toBeDefined();
expect(labelProp?.required).toBe(true);
});
it('extracts optional props', () => {
const entry = resolveComponent(parsed, baseConfig);
const sizeProp = entry.props.find((p) => p.name === 'size');
expect(sizeProp).toBeDefined();
expect(sizeProp?.required).toBe(false);
});
it('builds correct figma metadata', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.figma.fileKey).toBe('ABC123');
expect(entry.figma.nodeId).toBe('1:2');
expect(entry.figma.url).toBe(parsed.figmaUrl);
});
it('builds primary import statement', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.imports.primary).toContain('SimpleComponent');
expect(entry.imports.primary).toContain('@discourser/design-system');
});
});
describe('compound component', () => {
const parsed: ParsedFigmaFile = {
filePath: join(FIXTURES, 'CompoundComponent.figma.tsx'),
importStyle: 'namespace',
componentName: 'CompoundComponent',
importSource: './CompoundComponent/index',
connectSubComponent: 'Root',
figmaUrl: 'https://www.figma.com/design/ABC123/TestFile?node-id=5-10',
figmaFileKey: 'ABC123',
figmaNodeId: '5:10',
example: '...',
propsMapping: {},
};
it('classifies as compound', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.type).toBe('compound');
});
it('extracts sub-components from withContext/withProvider calls', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.subComponents).toBeDefined();
const names = entry.subComponents?.map((s) => s.name) ?? [];
expect(names).toContain('Root');
expect(names).toContain('Header');
expect(names).toContain('Body');
});
it('extracts HTML element for sub-components', () => {
const entry = resolveComponent(parsed, baseConfig);
const rootSub = entry.subComponents?.find((s) => s.name === 'Root');
expect(rootSub?.element).toBeTruthy();
});
it('builds namespace import statement', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.imports.primary).toContain('* as CompoundComponent');
});
});
describe('composite component detection', () => {
it('classifies NavigationMenu as composite (imports other DDS components)', () => {
const parsed: ParsedFigmaFile = {
filePath: join(
DDS_ROOT,
'src/components/NavigationMenu/NavigationMenu.figma.tsx',
),
importStyle: 'named',
componentName: 'NavigationMenu',
importSource: './NavigationMenu',
figmaUrl:
'https://www.figma.com/design/GaHmFfmvO4loUzuZS4TgEz/Discourser.AI--V1?node-id=38-8485',
figmaFileKey: 'GaHmFfmvO4loUzuZS4TgEz',
figmaNodeId: '38:8485',
example: '',
propsMapping: {},
};
const entry = resolveComponent(parsed, {
...baseConfig,
packageName: '@discourser/design-system',
});
expect(entry.type).toBe('composite');
});
});
describe('missing source file', () => {
it('returns empty props array when source file not found', () => {
const parsed: ParsedFigmaFile = {
filePath: '/nonexistent/dir/Missing.figma.tsx',
importStyle: 'named',
componentName: 'Missing',
importSource: './Missing',
figmaUrl: 'https://www.figma.com/design/X/F?node-id=1-1',
figmaFileKey: 'X',
figmaNodeId: '1:1',
example: '',
propsMapping: {},
};
const entry = resolveComponent(parsed, baseConfig);
expect(entry.props).toEqual([]);
});
});
describe('NavigationMenu multi-line prop types', () => {
const parsed: ParsedFigmaFile = {
filePath: join(
DDS_ROOT,
'src/components/NavigationMenu/NavigationMenu.figma.tsx',
),
importStyle: 'named',
componentName: 'NavigationMenu',
importSource: './NavigationMenu',
figmaUrl:
'https://www.figma.com/design/GaHmFfmvO4loUzuZS4TgEz/Discourser.AI--V1?node-id=38-8485',
figmaFileKey: 'GaHmFfmvO4loUzuZS4TgEz',
figmaNodeId: '38:8485',
example: '',
propsMapping: {},
};
it('extracts exactly 6 props (not leaking inner fields)', () => {
const entry = resolveComponent(parsed, baseConfig);
expect(entry.props).toHaveLength(6);
});
it('renderLink type contains full function signature', () => {
const entry = resolveComponent(parsed, baseConfig);
const renderLink = entry.props.find((p) => p.name === 'renderLink');
expect(renderLink).toBeDefined();
expect(renderLink?.type).toContain('=> React.ReactNode');
expect(renderLink?.type).toContain('href');
});
it('does not leak inner object fields as top-level props', () => {
const entry = resolveComponent(parsed, baseConfig);
const names = entry.props.map((p) => p.name);
expect(names).not.toContain('href');
expect(names).not.toContain('isActive');
expect(names).not.toContain('className');
});
});
});