Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions packages/figma/src/tests/transformers/tailwind/generators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ vi.mock('../../../theme-tokens', () => ({
},
borderRadius: {
'4px': 'DEFAULT',
'50%': 'full',
},
},
}));
Expand Down Expand Up @@ -255,6 +256,26 @@ describe('generateTailwindBorderClass', () => {
const result = generateTailwindBorderClass(borderTokenRgba);
expect(result).toBe('border-[rgba(0,0,0,0.03)]');
});

it('should use semantic variable name for border color when available', () => {
const borderTokenWithSemantic = {
...borderToken,
rawValue: '1px solid #4f5153',
semanticVariableName: 'navigation-border-neutral-footer-divider',
};
const result = generateTailwindBorderClass(borderTokenWithSemantic);
expect(result).toBe('border border-solid border-navigation-border-neutral-footer-divider');
});

it('should strip colors- prefix from semantic variable in border color', () => {
const borderTokenWithPrefix = {
...borderToken,
rawValue: '2px solid #4f5153',
semanticVariableName: 'colors-navigation-border-neutral-footer-divider',
};
const result = generateTailwindBorderClass(borderTokenWithPrefix);
expect(result).toBe('border-2 border-solid border-navigation-border-neutral-footer-divider');
});
});

describe('generateTailwindBorderRadiusClass', () => {
Expand All @@ -265,7 +286,7 @@ describe('generateTailwindBorderRadiusClass', () => {
};
it('should return tailwind utilities for border radius when given one property', () => {
const result = generateTailwindBorderRadiusClass(borderRadiusToken);
expect(result).toBe('rounded-tl-[20px] rounded-tr-[20px] rounded-br-[20px] rounded-bl-[20px]');
expect(result).toBe('rounded-[20px]');
});

it('should return tailwind utilities for border radius when given one property', () => {
Expand All @@ -275,7 +296,17 @@ describe('generateTailwindBorderRadiusClass', () => {
rawValue: '4px', // "1px" : "DEFAULT" in borderRadius
};
const result = generateTailwindBorderRadiusClass(defaultBorderRadiusToken);
expect(result).toBe('rounded-tl rounded-tr rounded-br rounded-bl');
expect(result).toBe('rounded');
});

it('should return rounded-full for 50% border radius (ellipse/circle elements)', () => {
const circularToken = {
...basicToken,
property: 'border-radius',
rawValue: '50%',
};
const result = generateTailwindBorderRadiusClass(circularToken);
expect(result).toBe('rounded-full');
});

it('should return tailwind utilities for border radius when given two properties', () => {
Expand Down Expand Up @@ -589,6 +620,38 @@ describe('generateTailwindBoxShadowClass', () => {
expect(result).toBe('shadow-[0_4px_6px_rgba(0,0,0,0.1),inset_0_1px_0_0_var(--blue-900)]');
});

it('should use semantic variable name for box-shadow color when available', () => {
const insetShadowToken = {
...boxShadowToken,
rawValue: 'inset 0 1px 0 0 #4f5153',
semanticVariableName: 'form-border-neutral-disabled',
};
const result = generateTailwindBoxShadowClass(insetShadowToken);
expect(result).toBe('shadow-[inset_0_1px_0_0_var(--form-border-neutral-disabled)]');
});

it('should strip colors- prefix from semantic variable in box-shadow color', () => {
const insetShadowToken = {
...boxShadowToken,
rawValue: 'inset 0 1px 0 0 #4f5153',
semanticVariableName: 'colors-form-border-neutral-disabled',
};
const result = generateTailwindBoxShadowClass(insetShadowToken);
expect(result).toBe('shadow-[inset_0_1px_0_0_var(--form-border-neutral-disabled)]');
});

it('should use semantic variable for all hex colors in multiple box shadows', () => {
const multiShadowToken = {
...boxShadowToken,
rawValue: 'inset 0 1px 0 0 #4f5153, inset -1px 0 0 0 #4f5153',
semanticVariableName: 'form-border-neutral-disabled',
};
const result = generateTailwindBoxShadowClass(multiShadowToken);
expect(result).toBe(
'shadow-[inset_0_1px_0_0_var(--form-border-neutral-disabled),inset_-1px_0_0_0_var(--form-border-neutral-disabled)]',
);
});

it('should use dynamic theme tokens when available', () => {
const shadowToken = {
...boxShadowToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { describe, it, expect } from 'vitest';
import { convertToGeneratorTokens } from '../../../transformers/variants/convert-to-generator-tokens';
import { StyleToken, VariableToken } from '../../../types';
import type { convertVariantGroupBy } from '../../../transformers/variants/convert-variant-group-by';

/**
* Helper to build a minimal parsedStyleToken input for convertToGeneratorTokens.
* Mirrors the shape returned by convertVariantGroupBy.
*/
function createParsedStyleToken({
property,
rawValue,
variableToken,
variableKey,
path = [],
}: {
property: string;
rawValue: string;
variableToken?: VariableToken;
/** The key under which the variable is stored in variableTokenMapByProperty */
variableKey?: string;
path?: SceneNode[];
}) {
const variableTokenMapByProperty = new Map<string, VariableToken>();
if (variableToken && variableKey) {
variableTokenMapByProperty.set(variableKey, variableToken);
}

const styleToken: StyleToken = {
type: 'style',
name: 'test',
value: rawValue,
rawValue,
valueType: 'px',
property,
path,
metadata: { figmaId: 'test-node' },
variableTokenMapByProperty,
};

return [
{
key: 'test-variant',
path,
componentId: null,
componentSetId: null,
variants: {},
styles: { [property]: rawValue },
tokens: [styleToken],
},
] as unknown as ReturnType<typeof convertVariantGroupBy>['selectors'];
}

function createVariableToken(
overrides: Partial<VariableToken> & { name: string; primitiveRef?: string },
): VariableToken {
return {
type: 'variable',
path: [],
property: 'color',
value: overrides.name,
rawValue: '#4f5153',
valueType: null,
metadata: {
figmaId: 'var-node',
variableId: 'var-1',
variableName: overrides.name,
variableTokenType: 'semantic',
},
...overrides,
} as VariableToken;
}

describe('convertToGeneratorTokens', () => {
describe('semantic variable name detection', () => {
it('should set semanticVariableName for a true alias (name !== primitiveRef)', () => {
const variableToken = createVariableToken({
name: 'navigation-border-neutral-footer-divider',
primitiveRef: 'gray-800',
});

const input = createParsedStyleToken({
property: 'border',
rawValue: '1px solid #4f5153',
variableToken,
variableKey: 'border',
});

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBe(
'navigation-border-neutral-footer-divider',
);
});

it('should NOT set semanticVariableName for a primitive (name === primitiveRef)', () => {
const variableToken = createVariableToken({
name: 'blue-500',
primitiveRef: 'blue-500',
});

const input = createParsedStyleToken({
property: 'background',
rawValue: '#0028a0',
variableToken,
variableKey: 'background',
});

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBeUndefined();
});

it('should set semanticVariableName when primitiveRef is undefined (legacy tokens)', () => {
const variableToken = createVariableToken({
name: 'action-bg-primary',
primitiveRef: undefined,
});

const input = createParsedStyleToken({
property: 'background',
rawValue: '#0028a0',
variableToken,
variableKey: 'background',
});

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBe('action-bg-primary');
});

it('should NOT set semanticVariableName when variableTokenType is primitive', () => {
const variableToken = createVariableToken({
name: 'gray-800',
primitiveRef: 'gray-800',
metadata: {
figmaId: 'var-node',
variableId: 'var-1',
variableName: 'gray-800',
variableTokenType: 'primitive',
},
});

const input = createParsedStyleToken({
property: 'color',
rawValue: '#4f5153',
variableToken,
variableKey: 'color',
});

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBeUndefined();
});
});

describe('box-shadow strokes fallback', () => {
it('should use strokes key for box-shadow semantic variable when property key has no variable', () => {
const strokeToken = createVariableToken({
name: 'form-border-neutral-disabled',
primitiveRef: 'gray-800',
});

const variableTokenMapByProperty = new Map<string, VariableToken>();
variableTokenMapByProperty.set('strokes', strokeToken);

const styleToken: StyleToken = {
type: 'style',
name: 'test',
value: 'inset 0 1px 0 0 #4f5153',
rawValue: 'inset 0 1px 0 0 #4f5153',
valueType: 'px',
property: 'box-shadow',
path: [],
metadata: { figmaId: 'test-node' },
variableTokenMapByProperty,
};

const input = [
{
key: 'test-variant',
path: [],
componentId: null,
componentSetId: null,
variants: {},
styles: { 'box-shadow': 'inset 0 1px 0 0 #4f5153' },
tokens: [styleToken],
},
] as unknown as ReturnType<
typeof import('../../../transformers/variants/convert-variant-group-by').convertVariantGroupBy
>['selectors'];

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBe('form-border-neutral-disabled');
});

it('should NOT use strokes key when the stroke variable is a primitive (name === primitiveRef)', () => {
const strokeToken = createVariableToken({
name: 'gray-800',
primitiveRef: 'gray-800',
});

const variableTokenMapByProperty = new Map<string, VariableToken>();
variableTokenMapByProperty.set('strokes', strokeToken);

const styleToken: StyleToken = {
type: 'style',
name: 'test',
value: 'inset 0 1px 0 0 #4f5153',
rawValue: 'inset 0 1px 0 0 #4f5153',
valueType: 'px',
property: 'box-shadow',
path: [],
metadata: { figmaId: 'test-node' },
variableTokenMapByProperty,
};

const input = [
{
key: 'test-variant',
path: [],
componentId: null,
componentSetId: null,
variants: {},
styles: { 'box-shadow': 'inset 0 1px 0 0 #4f5153' },
tokens: [styleToken],
},
] as unknown as ReturnType<
typeof import('../../../transformers/variants/convert-variant-group-by').convertVariantGroupBy
>['selectors'];

const result = convertToGeneratorTokens(input);
expect(result[0].tokens[0].semanticVariableName).toBeUndefined();
});
});
});
Loading