Skip to content

Commit 5d05d81

Browse files
committed
Add coverage
1 parent e10f9b5 commit 5d05d81

File tree

4 files changed

+259
-18
lines changed

4 files changed

+259
-18
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, mock, test } from 'bun:test'
2+
import { paintToCSS } from '../paint-to-css'
3+
4+
// mock asset checker to avoid real node handling
5+
mock.module('../check-asset-node', () => ({
6+
checkAssetNode: () => 'png',
7+
}))
8+
9+
describe('paintToCSS', () => {
10+
test('converts image paint with TILE scaleMode to repeat url', async () => {
11+
;(globalThis as { figma?: unknown }).figma = {
12+
util: { rgba: (v: unknown) => v },
13+
} as unknown as typeof figma
14+
15+
const res = await paintToCSS(
16+
{
17+
type: 'IMAGE',
18+
visible: true,
19+
opacity: 1,
20+
scaleMode: 'TILE',
21+
} as unknown as ImagePaint,
22+
{ width: 100, height: 100 } as unknown as SceneNode,
23+
false,
24+
)
25+
26+
expect(res).toBe('url(/icons/image.png) repeat')
27+
})
28+
29+
test('converts pattern paint with alignments and spacing', async () => {
30+
;(globalThis as { figma?: unknown }).figma = {
31+
getNodeByIdAsync: mock(() =>
32+
Promise.resolve({ name: 'patternNode' } as unknown as SceneNode),
33+
),
34+
} as unknown as typeof figma
35+
36+
const res = await paintToCSS(
37+
{
38+
type: 'PATTERN',
39+
visible: true,
40+
opacity: 1,
41+
sourceNodeId: '1',
42+
spacing: { x: 1, y: 2 },
43+
horizontalAlignment: 'CENTER',
44+
verticalAlignment: 'END',
45+
} as unknown as PatternPaint,
46+
{ width: 100, height: 100 } as unknown as SceneNode,
47+
false,
48+
)
49+
50+
expect(res).toBe(
51+
'url(/icons/patternNode.png) center 100% bottom 200% repeat',
52+
)
53+
})
54+
55+
test('returns null for unsupported paint type', async () => {
56+
const res = await paintToCSS(
57+
{ type: 'VIDEO' } as unknown as Paint,
58+
{ width: 10, height: 10 } as unknown as SceneNode,
59+
false,
60+
)
61+
expect(res).toBeNull()
62+
})
63+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { propsToString } from '../props-to-str'
3+
4+
describe('propsToString', () => {
5+
test('sorts uppercase keys first and formats booleans/strings', () => {
6+
const res = propsToString({ Z: '1', a: '2', b: false })
7+
expect(res.split(' ')).toContain('Z="1"')
8+
expect(res.split(' ')).toContain('b={false}')
9+
})
10+
11+
test('omits assignment for boolean true', () => {
12+
const res = propsToString({ Active: true, label: 'ok' })
13+
expect(res).toContain('Active')
14+
expect(res).not.toContain('Active={true}')
15+
})
16+
17+
test('sort comparator handles lower then upper keys', () => {
18+
const res = propsToString({ b: '1', A: '2' })
19+
expect(res.startsWith('A="2"')).toBe(true)
20+
})
21+
22+
test('formats objects with newlines when many props', () => {
23+
const res = propsToString({
24+
a: '1',
25+
b: { x: 1 },
26+
c: '3',
27+
d: '4',
28+
e: '5',
29+
})
30+
expect(res).toContain('\n')
31+
expect(res).toContain(`b={${JSON.stringify({ x: 1 }, null, 2)}}`)
32+
})
33+
34+
test('handles empty props', () => {
35+
expect(propsToString({})).toBe('')
36+
})
37+
})

src/codegen/utils/props-to-str.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
export function propsToString(props: Record<string, unknown>) {
2-
return Object.entries(props)
3-
.sort((a, b) => {
4-
const isAUpper = /^[A-Z]/.test(a[0])
5-
const isBUpper = /^[A-Z]/.test(b[0])
6-
if (isAUpper && !isBUpper) return -1
7-
if (!isAUpper && isBUpper) return 1
8-
return a[0].localeCompare(b[0])
9-
})
10-
.map(
11-
([key, value]) =>
12-
`${key}${typeof value === 'boolean' ? (value ? '' : `={${value}}`) : typeof value === 'object' ? `={${JSON.stringify(value, null, 2)}}` : `="${value}"`}`,
13-
)
14-
.join(
15-
Object.keys(props).length >= 5 ||
16-
Object.values(props).some((value) => typeof value === 'object')
17-
? '\n'
18-
: ' ',
19-
)
2+
const sorted = Object.entries(props).sort((a, b) => {
3+
const isAUpper = /^[A-Z]/.test(a[0])
4+
const isBUpper = /^[A-Z]/.test(b[0])
5+
if (isAUpper && !isBUpper) return -1
6+
if (!isAUpper && isBUpper) return 1
7+
return a[0].localeCompare(b[0])
8+
})
9+
10+
const parts = sorted.map(([key, value]) => {
11+
if (typeof value === 'boolean') return `${key}${value ? '' : `={${value}}`}`
12+
if (typeof value === 'object')
13+
return `${key}={${JSON.stringify(value, null, 2)}}`
14+
return `${key}="${value}"`
15+
})
16+
17+
const separator =
18+
Object.keys(props).length >= 5 ||
19+
Object.values(props).some((value) => typeof value === 'object')
20+
? '\n'
21+
: ' '
22+
return parts.join(separator)
2023
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, expect, mock, spyOn, test } from 'bun:test'
2+
import * as uploadFileModule from '../../../utils/upload-file'
3+
import { importDevup } from '../import-devup'
4+
import * as uploadXlsxModule from '../utils/upload-devup-xlsx'
5+
6+
describe('import-devup (standalone file)', () => {
7+
test('returns early when theme is missing', async () => {
8+
const uploadFile = mock(() => Promise.resolve('{}'))
9+
spyOn(uploadFileModule, 'uploadFile').mockImplementation(uploadFile)
10+
await importDevup('json')
11+
expect(uploadFile).toHaveBeenCalledWith('.json')
12+
})
13+
14+
test('imports colors and typography from excel payload', async () => {
15+
const uploadXlsx = mock(() =>
16+
Promise.resolve({
17+
theme: {
18+
colors: { Light: { primary: '#111111' } },
19+
typography: {
20+
heading: {
21+
fontFamily: 'Inter',
22+
fontStyle: 'italic',
23+
fontSize: '12',
24+
letterSpacing: '0.1em',
25+
lineHeight: 'normal',
26+
textTransform: 'uppercase',
27+
textDecoration: 'underline',
28+
},
29+
},
30+
},
31+
}),
32+
)
33+
spyOn(uploadXlsxModule, 'uploadDevupXlsx').mockImplementation(uploadXlsx)
34+
35+
const setValueForMode = mock(() => {})
36+
const createVariable = mock(
37+
() =>
38+
({
39+
name: 'primary',
40+
setValueForMode,
41+
remove: mock(() => {}),
42+
}) as unknown as Variable,
43+
)
44+
const addMode = mock((name: string) => `${name}-id`)
45+
const collection = {
46+
modes: [] as { modeId: string; name: string }[],
47+
addMode,
48+
removeMode: mock(() => {}),
49+
} as unknown as VariableCollection
50+
51+
const createTextStyle = mock(
52+
() =>
53+
({
54+
name: '',
55+
}) as unknown as TextStyle,
56+
)
57+
const loadFontAsync = mock(() => Promise.resolve())
58+
59+
;(globalThis as { figma?: unknown }).figma = {
60+
util: { rgba: (v: unknown) => v },
61+
variables: {
62+
getLocalVariableCollectionsAsync: async () => [],
63+
getLocalVariablesAsync: async () => [],
64+
createVariableCollection: () => collection,
65+
createVariable,
66+
},
67+
getLocalTextStylesAsync: async () => [],
68+
createTextStyle,
69+
loadFontAsync,
70+
} as unknown as typeof figma
71+
72+
await importDevup('excel')
73+
74+
expect(addMode).toHaveBeenCalledWith('Light')
75+
expect(setValueForMode).toHaveBeenCalledWith('Light-id', '#111111')
76+
expect(createTextStyle).toHaveBeenCalled()
77+
expect(loadFontAsync).toHaveBeenCalled()
78+
})
79+
80+
test('imports array typography and skips nulls', async () => {
81+
const uploadXlsx = mock(() =>
82+
Promise.resolve({
83+
theme: {
84+
typography: {
85+
title: [
86+
null,
87+
{
88+
fontFamily: 'Inter',
89+
fontSize: '14',
90+
letterSpacing: '1',
91+
lineHeight: 120,
92+
},
93+
],
94+
},
95+
},
96+
}),
97+
)
98+
spyOn(uploadXlsxModule, 'uploadDevupXlsx').mockImplementation(uploadXlsx)
99+
100+
const createTextStyle = mock(
101+
() =>
102+
({
103+
name: '',
104+
}) as unknown as TextStyle,
105+
)
106+
const loadFontAsync = mock(() => Promise.resolve())
107+
108+
;(globalThis as { figma?: unknown }).figma = {
109+
util: { rgba: (v: unknown) => v },
110+
variables: {
111+
getLocalVariableCollectionsAsync: async () => [],
112+
getLocalVariablesAsync: async () => [],
113+
createVariableCollection: () =>
114+
({
115+
modes: [],
116+
addMode: mock(() => 'm1'),
117+
removeMode: mock(() => {}),
118+
}) as unknown as VariableCollection,
119+
createVariable: mock(
120+
() =>
121+
({
122+
name: 'primary',
123+
setValueForMode: mock(() => {}),
124+
remove: mock(() => {}),
125+
}) as unknown as Variable,
126+
),
127+
},
128+
getLocalTextStylesAsync: async () => [],
129+
createTextStyle,
130+
loadFontAsync,
131+
} as unknown as typeof figma
132+
133+
await importDevup('excel')
134+
135+
expect(createTextStyle).toHaveBeenCalled()
136+
expect(loadFontAsync).toHaveBeenCalled()
137+
})
138+
})

0 commit comments

Comments
 (0)