Fill Figma Plugin API gaps: 29 new tests, 30+ new properties and methods
SceneNode: add minWidth/maxWidth/minHeight/maxHeight, isMask, maskType, counterAxisAlignContent, itemReverseZIndex, strokesIncludedInLayout, expanded, textTruncation, autoRename, strokeMiterLimit. All with defaults and .fig import mapping. FigmaNodeProxy: add clone(), strokeCap, strokeJoin, strokeMiterLimit, strokeTopWeight/BottomWeight/LeftWeight/RightWeight, minWidth/maxWidth/ minHeight/maxHeight, isMask, maskType, expanded, textTruncation, autoRename, insertCharacters, deleteCharacters, primaryAxisSizingMode, counterAxisSizingMode, counterAxisAlignContent, itemReverseZIndex, strokesIncludedInLayout, layoutAlign, findAllWithCriteria. FigmaAPI: add createComponentFromNode, getVariableById, getLocalVariables, getLocalVariableCollections, getVariableCollectionById. Fix layoutSizingHorizontal/Vertical for self-framing auto-layout nodes with no auto-layout parent — now checks the node's own layoutMode.
This commit is contained in:
parent
850a1179b3
commit
d08eb4f3b4
|
|
@ -6,6 +6,8 @@ import type {
|
|||
Stroke,
|
||||
Effect,
|
||||
LayoutMode,
|
||||
Variable,
|
||||
VariableCollection,
|
||||
} from './scene-graph'
|
||||
import { SceneGraph } from './scene-graph'
|
||||
|
||||
|
|
@ -307,12 +309,68 @@ class FigmaNodeProxy {
|
|||
}
|
||||
}
|
||||
|
||||
get dashPattern(): number[] {
|
||||
return this._raw().dashPattern
|
||||
get dashPattern(): readonly number[] {
|
||||
return Object.freeze([...this._raw().dashPattern])
|
||||
}
|
||||
|
||||
set dashPattern(v: number[]) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { dashPattern: v })
|
||||
set dashPattern(v: readonly number[]) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { dashPattern: [...v] })
|
||||
}
|
||||
|
||||
get strokeCap(): string {
|
||||
return this._raw().strokeCap
|
||||
}
|
||||
|
||||
set strokeCap(v: string) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeCap: v as SceneNode['strokeCap'] })
|
||||
}
|
||||
|
||||
get strokeJoin(): string {
|
||||
return this._raw().strokeJoin
|
||||
}
|
||||
|
||||
set strokeJoin(v: string) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeJoin: v as SceneNode['strokeJoin'] })
|
||||
}
|
||||
|
||||
get strokeMiterLimit(): number {
|
||||
return this._raw().strokeMiterLimit
|
||||
}
|
||||
|
||||
set strokeMiterLimit(v: number) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokeMiterLimit: v })
|
||||
}
|
||||
|
||||
get strokeTopWeight(): number {
|
||||
return this._raw().borderTopWeight
|
||||
}
|
||||
|
||||
set strokeTopWeight(v: number) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { borderTopWeight: v, independentStrokeWeights: true })
|
||||
}
|
||||
|
||||
get strokeBottomWeight(): number {
|
||||
return this._raw().borderBottomWeight
|
||||
}
|
||||
|
||||
set strokeBottomWeight(v: number) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { borderBottomWeight: v, independentStrokeWeights: true })
|
||||
}
|
||||
|
||||
get strokeLeftWeight(): number {
|
||||
return this._raw().borderLeftWeight
|
||||
}
|
||||
|
||||
set strokeLeftWeight(v: number) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { borderLeftWeight: v, independentStrokeWeights: true })
|
||||
}
|
||||
|
||||
get strokeRightWeight(): number {
|
||||
return this._raw().borderRightWeight
|
||||
}
|
||||
|
||||
set strokeRightWeight(v: number) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { borderRightWeight: v, independentStrokeWeights: true })
|
||||
}
|
||||
|
||||
// --- Text ---
|
||||
|
|
@ -415,6 +473,34 @@ class FigmaNodeProxy {
|
|||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxLines: v })
|
||||
}
|
||||
|
||||
get textTruncation(): string {
|
||||
return this._raw().textTruncation
|
||||
}
|
||||
|
||||
set textTruncation(v: string) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { textTruncation: v as SceneNode['textTruncation'] })
|
||||
}
|
||||
|
||||
get autoRename(): boolean {
|
||||
return this._raw().autoRename
|
||||
}
|
||||
|
||||
set autoRename(v: boolean) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { autoRename: v })
|
||||
}
|
||||
|
||||
insertCharacters(start: number, characters: string): void {
|
||||
const n = this._raw()
|
||||
const text = n.text.slice(0, start) + characters + n.text.slice(start)
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { text })
|
||||
}
|
||||
|
||||
deleteCharacters(start: number, end: number): void {
|
||||
const n = this._raw()
|
||||
const text = n.text.slice(0, start) + n.text.slice(end)
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { text })
|
||||
}
|
||||
|
||||
// --- Auto-layout ---
|
||||
|
||||
get layoutMode(): LayoutMode {
|
||||
|
|
@ -497,6 +583,48 @@ class FigmaNodeProxy {
|
|||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutWrap: v as SceneNode['layoutWrap'] })
|
||||
}
|
||||
|
||||
get primaryAxisSizingMode(): string {
|
||||
return this._raw().primaryAxisSizing === 'HUG' ? 'AUTO' : this._raw().primaryAxisSizing
|
||||
}
|
||||
|
||||
set primaryAxisSizingMode(v: string) {
|
||||
const mapped = v === 'AUTO' ? 'HUG' : v
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { primaryAxisSizing: mapped as SceneNode['primaryAxisSizing'] })
|
||||
}
|
||||
|
||||
get counterAxisSizingMode(): string {
|
||||
return this._raw().counterAxisSizing === 'HUG' ? 'AUTO' : this._raw().counterAxisSizing
|
||||
}
|
||||
|
||||
set counterAxisSizingMode(v: string) {
|
||||
const mapped = v === 'AUTO' ? 'HUG' : v
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { counterAxisSizing: mapped as SceneNode['counterAxisSizing'] })
|
||||
}
|
||||
|
||||
get counterAxisAlignContent(): string {
|
||||
return this._raw().counterAxisAlignContent
|
||||
}
|
||||
|
||||
set counterAxisAlignContent(v: string) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { counterAxisAlignContent: v as SceneNode['counterAxisAlignContent'] })
|
||||
}
|
||||
|
||||
get itemReverseZIndex(): boolean {
|
||||
return this._raw().itemReverseZIndex
|
||||
}
|
||||
|
||||
set itemReverseZIndex(v: boolean) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { itemReverseZIndex: v })
|
||||
}
|
||||
|
||||
get strokesIncludedInLayout(): boolean {
|
||||
return this._raw().strokesIncludedInLayout
|
||||
}
|
||||
|
||||
set strokesIncludedInLayout(v: boolean) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { strokesIncludedInLayout: v })
|
||||
}
|
||||
|
||||
// --- Layout child props ---
|
||||
|
||||
get layoutPositioning(): string {
|
||||
|
|
@ -515,17 +643,29 @@ class FigmaNodeProxy {
|
|||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutGrow: v })
|
||||
}
|
||||
|
||||
get layoutAlign(): string {
|
||||
const n = this._raw()
|
||||
return n.layoutAlignSelf === 'STRETCH' ? 'STRETCH' : 'INHERIT'
|
||||
}
|
||||
|
||||
set layoutAlign(v: string) {
|
||||
const mapped = v === 'STRETCH' ? 'STRETCH' : 'AUTO'
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { layoutAlignSelf: mapped as SceneNode['layoutAlignSelf'] })
|
||||
}
|
||||
|
||||
get layoutSizingHorizontal(): string {
|
||||
const n = this._raw()
|
||||
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
||||
if (!parent || parent.layoutMode === 'NONE') return n.primaryAxisSizing
|
||||
return parent.layoutMode === 'HORIZONTAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
||||
const refLayout = (parent && parent.layoutMode !== 'NONE') ? parent.layoutMode : n.layoutMode
|
||||
if (refLayout === 'NONE') return 'FIXED'
|
||||
return refLayout === 'HORIZONTAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
||||
}
|
||||
|
||||
set layoutSizingHorizontal(v: string) {
|
||||
const n = this._raw()
|
||||
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
||||
if (parent && parent.layoutMode === 'VERTICAL') {
|
||||
const refLayout = (parent && parent.layoutMode !== 'NONE') ? parent.layoutMode : n.layoutMode
|
||||
if (refLayout === 'VERTICAL') {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { counterAxisSizing: v as SceneNode['counterAxisSizing'] })
|
||||
} else {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { primaryAxisSizing: v as SceneNode['primaryAxisSizing'] })
|
||||
|
|
@ -535,14 +675,16 @@ class FigmaNodeProxy {
|
|||
get layoutSizingVertical(): string {
|
||||
const n = this._raw()
|
||||
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
||||
if (!parent || parent.layoutMode === 'NONE') return n.counterAxisSizing
|
||||
return parent.layoutMode === 'VERTICAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
||||
const refLayout = (parent && parent.layoutMode !== 'NONE') ? parent.layoutMode : n.layoutMode
|
||||
if (refLayout === 'NONE') return 'FIXED'
|
||||
return refLayout === 'VERTICAL' ? n.primaryAxisSizing : n.counterAxisSizing
|
||||
}
|
||||
|
||||
set layoutSizingVertical(v: string) {
|
||||
const n = this._raw()
|
||||
const parent = n.parentId ? this[INTERNAL_GRAPH].getNode(n.parentId) : undefined
|
||||
if (parent && parent.layoutMode === 'HORIZONTAL') {
|
||||
const refLayout = (parent && parent.layoutMode !== 'NONE') ? parent.layoutMode : n.layoutMode
|
||||
if (refLayout === 'HORIZONTAL') {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { counterAxisSizing: v as SceneNode['counterAxisSizing'] })
|
||||
} else {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { primaryAxisSizing: v as SceneNode['primaryAxisSizing'] })
|
||||
|
|
@ -563,6 +705,68 @@ class FigmaNodeProxy {
|
|||
})
|
||||
}
|
||||
|
||||
// --- Dimension constraints ---
|
||||
|
||||
get minWidth(): number | null {
|
||||
return this._raw().minWidth
|
||||
}
|
||||
|
||||
set minWidth(v: number | null) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { minWidth: v })
|
||||
}
|
||||
|
||||
get maxWidth(): number | null {
|
||||
return this._raw().maxWidth
|
||||
}
|
||||
|
||||
set maxWidth(v: number | null) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxWidth: v })
|
||||
}
|
||||
|
||||
get minHeight(): number | null {
|
||||
return this._raw().minHeight
|
||||
}
|
||||
|
||||
set minHeight(v: number | null) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { minHeight: v })
|
||||
}
|
||||
|
||||
get maxHeight(): number | null {
|
||||
return this._raw().maxHeight
|
||||
}
|
||||
|
||||
set maxHeight(v: number | null) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maxHeight: v })
|
||||
}
|
||||
|
||||
// --- Mask ---
|
||||
|
||||
get isMask(): boolean {
|
||||
return this._raw().isMask
|
||||
}
|
||||
|
||||
set isMask(v: boolean) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { isMask: v })
|
||||
}
|
||||
|
||||
get maskType(): string {
|
||||
return this._raw().maskType
|
||||
}
|
||||
|
||||
set maskType(v: string) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { maskType: v as SceneNode['maskType'] })
|
||||
}
|
||||
|
||||
// --- UI state ---
|
||||
|
||||
get expanded(): boolean {
|
||||
return this._raw().expanded
|
||||
}
|
||||
|
||||
set expanded(v: boolean) {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { expanded: v })
|
||||
}
|
||||
|
||||
// --- Components ---
|
||||
|
||||
get mainComponent(): FigmaNodeProxy | null {
|
||||
|
|
@ -605,6 +809,14 @@ class FigmaNodeProxy {
|
|||
this[INTERNAL_GRAPH].reorderChild(child[INTERNAL_ID], this[INTERNAL_ID], index)
|
||||
}
|
||||
|
||||
clone(): FigmaNodeProxy {
|
||||
const n = this._raw()
|
||||
const parentId = n.parentId ?? this[INTERNAL_API].currentPageId
|
||||
const cloned = this[INTERNAL_GRAPH].cloneTree(this[INTERNAL_ID], parentId)
|
||||
if (!cloned) throw new Error(`Failed to clone node ${this[INTERNAL_ID]}`)
|
||||
return this[INTERNAL_API].wrapNode(cloned.id)
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this[INTERNAL_GRAPH].deleteNode(this[INTERNAL_ID])
|
||||
}
|
||||
|
|
@ -650,6 +862,11 @@ class FigmaNodeProxy {
|
|||
.filter((proxy) => !callback || callback(proxy))
|
||||
}
|
||||
|
||||
findAllWithCriteria(criteria: { types?: string[] }): FigmaNodeProxy[] {
|
||||
const types = criteria.types ? new Set(criteria.types) : null
|
||||
return this.findAll((node) => !types || types.has(node.type))
|
||||
}
|
||||
|
||||
// --- Serialization ---
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
|
|
@ -826,6 +1043,63 @@ export class FigmaAPI {
|
|||
this.graph.deleteNode(node[INTERNAL_ID])
|
||||
}
|
||||
|
||||
createComponentFromNode(node: FigmaNodeProxy): FigmaNodeProxy {
|
||||
const raw = this.graph.getNode(node[INTERNAL_ID])
|
||||
if (!raw) throw new Error('Node not found')
|
||||
const parentId = raw.parentId ?? this._currentPageId
|
||||
const comp = this.graph.createNode('COMPONENT', parentId)
|
||||
this.graph.updateNode(comp.id, {
|
||||
name: raw.name,
|
||||
width: raw.width,
|
||||
height: raw.height,
|
||||
x: raw.x,
|
||||
y: raw.y,
|
||||
fills: structuredClone(raw.fills),
|
||||
strokes: structuredClone(raw.strokes),
|
||||
effects: structuredClone(raw.effects),
|
||||
cornerRadius: raw.cornerRadius,
|
||||
topLeftRadius: raw.topLeftRadius,
|
||||
topRightRadius: raw.topRightRadius,
|
||||
bottomRightRadius: raw.bottomRightRadius,
|
||||
bottomLeftRadius: raw.bottomLeftRadius,
|
||||
independentCorners: raw.independentCorners,
|
||||
opacity: raw.opacity,
|
||||
layoutMode: raw.layoutMode,
|
||||
primaryAxisAlign: raw.primaryAxisAlign,
|
||||
counterAxisAlign: raw.counterAxisAlign,
|
||||
itemSpacing: raw.itemSpacing,
|
||||
paddingTop: raw.paddingTop,
|
||||
paddingRight: raw.paddingRight,
|
||||
paddingBottom: raw.paddingBottom,
|
||||
paddingLeft: raw.paddingLeft,
|
||||
})
|
||||
for (const childId of raw.childIds) {
|
||||
this.graph.cloneTree(childId, comp.id)
|
||||
}
|
||||
this.graph.deleteNode(node[INTERNAL_ID])
|
||||
return this.wrapNode(comp.id)
|
||||
}
|
||||
|
||||
// --- Variables ---
|
||||
|
||||
getVariableById(id: string): Variable | null {
|
||||
return this.graph.variables.get(id) ?? null
|
||||
}
|
||||
|
||||
getLocalVariables(type?: string): Variable[] {
|
||||
const vars = [...this.graph.variables.values()]
|
||||
if (type) return vars.filter((v) => v.type === type)
|
||||
return vars
|
||||
}
|
||||
|
||||
getLocalVariableCollections(): VariableCollection[] {
|
||||
return [...this.graph.variableCollections.values()]
|
||||
}
|
||||
|
||||
getVariableCollectionById(id: string): VariableCollection | null {
|
||||
return this.graph.variableCollections.get(id) ?? null
|
||||
}
|
||||
|
||||
// --- Stubs ---
|
||||
|
||||
async loadFontAsync(_fontName: FigmaFontName): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export {
|
|||
type Stroke,
|
||||
type StrokeCap,
|
||||
type StrokeJoin,
|
||||
type MaskType,
|
||||
type Effect,
|
||||
type BlendMode,
|
||||
type ImageScaleMode,
|
||||
|
|
|
|||
|
|
@ -478,6 +478,7 @@ export function importNodeChanges(
|
|||
counterAxisSpacing: (ext(nc).stackCounterSpacing as number) ?? 0,
|
||||
layoutPositioning: ext(nc).stackPositioning === 'ABSOLUTE' ? 'ABSOLUTE' : 'AUTO',
|
||||
layoutGrow: (ext(nc).stackChildPrimaryGrow as number) ?? 0,
|
||||
layoutAlignSelf: (ext(nc).stackChildAlignSelf as string) === 'STRETCH' ? 'STRETCH' : 'AUTO',
|
||||
vectorNetwork: resolveVectorNetwork(nc, blobs),
|
||||
arcData: mapArcData(ext(nc).arcData as Record<string, number> | undefined),
|
||||
strokeCap: (nc.strokeCap ?? 'NONE') as StrokeCap,
|
||||
|
|
@ -488,6 +489,19 @@ export function importNodeChanges(
|
|||
borderBottomWeight: (ext(nc).borderBottomWeight as number) ?? 0,
|
||||
borderLeftWeight: (ext(nc).borderLeftWeight as number) ?? 0,
|
||||
independentStrokeWeights: (ext(nc).borderStrokeWeightsIndependent as boolean) ?? false,
|
||||
strokeMiterLimit: (ext(nc).strokeMiterAngle as number) ?? 4,
|
||||
minWidth: (ext(nc).minWidth as number) ?? null,
|
||||
maxWidth: (ext(nc).maxWidth as number) ?? null,
|
||||
minHeight: (ext(nc).minHeight as number) ?? null,
|
||||
maxHeight: (ext(nc).maxHeight as number) ?? null,
|
||||
isMask: (ext(nc).isMask as boolean) ?? false,
|
||||
maskType: ((ext(nc).maskType as string) ?? 'ALPHA') as 'ALPHA' | 'VECTOR' | 'LUMINANCE',
|
||||
counterAxisAlignContent: (ext(nc).stackCounterAlignContent as string) === 'SPACE_BETWEEN' ? 'SPACE_BETWEEN' : 'AUTO',
|
||||
itemReverseZIndex: (ext(nc).stackReverseZIndex as boolean) ?? false,
|
||||
strokesIncludedInLayout: (ext(nc).strokesIncludedInLayout as boolean) ?? false,
|
||||
expanded: nc.frameMaskDisabled !== true,
|
||||
textTruncation: (ext(nc).textTruncation as string) === 'ENDING' ? 'ENDING' : 'DISABLED',
|
||||
autoRename: (ext(nc).autoRename as boolean) ?? true,
|
||||
boundVariables: extractBoundVariables(nc)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export interface Fill {
|
|||
|
||||
export type StrokeCap = 'NONE' | 'ROUND' | 'SQUARE' | 'ARROW_LINES' | 'ARROW_EQUILATERAL'
|
||||
export type StrokeJoin = 'MITER' | 'BEVEL' | 'ROUND'
|
||||
export type MaskType = 'ALPHA' | 'VECTOR' | 'LUMINANCE'
|
||||
|
||||
export interface Stroke {
|
||||
color: Color
|
||||
|
|
@ -237,6 +238,24 @@ export interface SceneNode {
|
|||
borderLeftWeight: number
|
||||
independentStrokeWeights: boolean
|
||||
|
||||
strokeMiterLimit: number
|
||||
|
||||
minWidth: number | null
|
||||
maxWidth: number | null
|
||||
minHeight: number | null
|
||||
maxHeight: number | null
|
||||
|
||||
isMask: boolean
|
||||
maskType: MaskType
|
||||
|
||||
counterAxisAlignContent: 'AUTO' | 'SPACE_BETWEEN'
|
||||
itemReverseZIndex: boolean
|
||||
strokesIncludedInLayout: boolean
|
||||
|
||||
expanded: boolean
|
||||
textTruncation: 'DISABLED' | 'ENDING'
|
||||
autoRename: boolean
|
||||
|
||||
pointCount: number
|
||||
starInnerRadius: number
|
||||
|
||||
|
|
@ -346,6 +365,19 @@ function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}):
|
|||
borderBottomWeight: 0,
|
||||
borderLeftWeight: 0,
|
||||
independentStrokeWeights: false,
|
||||
strokeMiterLimit: 4,
|
||||
minWidth: null,
|
||||
maxWidth: null,
|
||||
minHeight: null,
|
||||
maxHeight: null,
|
||||
isMask: false,
|
||||
maskType: 'ALPHA',
|
||||
counterAxisAlignContent: 'AUTO',
|
||||
itemReverseZIndex: false,
|
||||
strokesIncludedInLayout: false,
|
||||
expanded: true,
|
||||
textTruncation: 'DISABLED',
|
||||
autoRename: true,
|
||||
pointCount: 5,
|
||||
starInnerRadius: 0.38,
|
||||
componentId: null,
|
||||
|
|
|
|||
|
|
@ -588,5 +588,337 @@ describe('FigmaAPI', () => {
|
|||
rect.strokeWeight = 4
|
||||
expect(rect.strokeWeight).toBe(4)
|
||||
})
|
||||
|
||||
test('strokeCap and strokeJoin', () => {
|
||||
const api = createAPI()
|
||||
const line = api.createLine()
|
||||
expect(line.strokeCap).toBe('NONE')
|
||||
expect(line.strokeJoin).toBe('MITER')
|
||||
line.strokeCap = 'ROUND'
|
||||
line.strokeJoin = 'BEVEL'
|
||||
expect(line.strokeCap).toBe('ROUND')
|
||||
expect(line.strokeJoin).toBe('BEVEL')
|
||||
})
|
||||
|
||||
test('strokeMiterLimit', () => {
|
||||
const api = createAPI()
|
||||
const rect = api.createRectangle()
|
||||
expect(rect.strokeMiterLimit).toBe(4)
|
||||
rect.strokeMiterLimit = 8
|
||||
expect(rect.strokeMiterLimit).toBe(8)
|
||||
})
|
||||
|
||||
test('individual stroke weights', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.strokeTopWeight = 1
|
||||
frame.strokeRightWeight = 2
|
||||
frame.strokeBottomWeight = 3
|
||||
frame.strokeLeftWeight = 4
|
||||
expect(frame.strokeTopWeight).toBe(1)
|
||||
expect(frame.strokeRightWeight).toBe(2)
|
||||
expect(frame.strokeBottomWeight).toBe(3)
|
||||
expect(frame.strokeLeftWeight).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clone', () => {
|
||||
test('clone creates a deep copy', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.name = 'Original'
|
||||
frame.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
const child = api.createRectangle()
|
||||
child.name = 'Child'
|
||||
frame.appendChild(child)
|
||||
|
||||
const cloned = frame.clone()
|
||||
expect(cloned.id).not.toBe(frame.id)
|
||||
expect(cloned.name).toBe('Original')
|
||||
expect(cloned.fills[0].color.r).toBe(1)
|
||||
expect(cloned.children.length).toBe(1)
|
||||
expect(cloned.children[0].name).toBe('Child')
|
||||
expect(cloned.children[0].id).not.toBe(child.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('min/max dimensions', () => {
|
||||
test('defaults to null', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
expect(frame.minWidth).toBeNull()
|
||||
expect(frame.maxWidth).toBeNull()
|
||||
expect(frame.minHeight).toBeNull()
|
||||
expect(frame.maxHeight).toBeNull()
|
||||
})
|
||||
|
||||
test('can set and read', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.minWidth = 50
|
||||
frame.maxWidth = 400
|
||||
frame.minHeight = 30
|
||||
frame.maxHeight = 600
|
||||
expect(frame.minWidth).toBe(50)
|
||||
expect(frame.maxWidth).toBe(400)
|
||||
expect(frame.minHeight).toBe(30)
|
||||
expect(frame.maxHeight).toBe(600)
|
||||
})
|
||||
|
||||
test('can reset to null', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.minWidth = 50
|
||||
frame.minWidth = null
|
||||
expect(frame.minWidth).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mask', () => {
|
||||
test('isMask defaults to false', () => {
|
||||
const api = createAPI()
|
||||
const rect = api.createRectangle()
|
||||
expect(rect.isMask).toBe(false)
|
||||
expect(rect.maskType).toBe('ALPHA')
|
||||
})
|
||||
|
||||
test('can set mask properties', () => {
|
||||
const api = createAPI()
|
||||
const rect = api.createRectangle()
|
||||
rect.isMask = true
|
||||
rect.maskType = 'LUMINANCE'
|
||||
expect(rect.isMask).toBe(true)
|
||||
expect(rect.maskType).toBe('LUMINANCE')
|
||||
})
|
||||
})
|
||||
|
||||
describe('auto-layout extras', () => {
|
||||
test('primaryAxisSizingMode maps FIXED/AUTO', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.layoutMode = 'VERTICAL'
|
||||
expect(frame.primaryAxisSizingMode).toBe('FIXED')
|
||||
frame.primaryAxisSizingMode = 'AUTO'
|
||||
expect(frame.primaryAxisSizingMode).toBe('AUTO')
|
||||
expect(frame.layoutSizingVertical).toBe('HUG')
|
||||
})
|
||||
|
||||
test('counterAxisSizingMode maps FIXED/AUTO', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.layoutMode = 'VERTICAL'
|
||||
frame.counterAxisSizingMode = 'AUTO'
|
||||
expect(frame.counterAxisSizingMode).toBe('AUTO')
|
||||
expect(frame.layoutSizingHorizontal).toBe('HUG')
|
||||
})
|
||||
|
||||
test('counterAxisAlignContent', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
expect(frame.counterAxisAlignContent).toBe('AUTO')
|
||||
frame.counterAxisAlignContent = 'SPACE_BETWEEN'
|
||||
expect(frame.counterAxisAlignContent).toBe('SPACE_BETWEEN')
|
||||
})
|
||||
|
||||
test('itemReverseZIndex', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
expect(frame.itemReverseZIndex).toBe(false)
|
||||
frame.itemReverseZIndex = true
|
||||
expect(frame.itemReverseZIndex).toBe(true)
|
||||
})
|
||||
|
||||
test('strokesIncludedInLayout', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
expect(frame.strokesIncludedInLayout).toBe(false)
|
||||
frame.strokesIncludedInLayout = true
|
||||
expect(frame.strokesIncludedInLayout).toBe(true)
|
||||
})
|
||||
|
||||
test('layoutAlign maps to layoutAlignSelf', () => {
|
||||
const api = createAPI()
|
||||
const parent = api.createFrame()
|
||||
parent.layoutMode = 'VERTICAL'
|
||||
const child = api.createFrame()
|
||||
parent.appendChild(child)
|
||||
expect(child.layoutAlign).toBe('INHERIT')
|
||||
child.layoutAlign = 'STRETCH'
|
||||
expect(child.layoutAlign).toBe('STRETCH')
|
||||
})
|
||||
})
|
||||
|
||||
describe('text extras', () => {
|
||||
test('textTruncation', () => {
|
||||
const api = createAPI()
|
||||
const text = api.createText()
|
||||
expect(text.textTruncation).toBe('DISABLED')
|
||||
text.textTruncation = 'ENDING'
|
||||
expect(text.textTruncation).toBe('ENDING')
|
||||
})
|
||||
|
||||
test('autoRename', () => {
|
||||
const api = createAPI()
|
||||
const text = api.createText()
|
||||
expect(text.autoRename).toBe(true)
|
||||
text.autoRename = false
|
||||
expect(text.autoRename).toBe(false)
|
||||
})
|
||||
|
||||
test('insertCharacters', () => {
|
||||
const api = createAPI()
|
||||
const text = api.createText()
|
||||
text.characters = 'Hello World'
|
||||
text.insertCharacters(5, ' Beautiful')
|
||||
expect(text.characters).toBe('Hello Beautiful World')
|
||||
})
|
||||
|
||||
test('deleteCharacters', () => {
|
||||
const api = createAPI()
|
||||
const text = api.createText()
|
||||
text.characters = 'Hello Beautiful World'
|
||||
text.deleteCharacters(5, 15)
|
||||
expect(text.characters).toBe('Hello World')
|
||||
})
|
||||
})
|
||||
|
||||
describe('expanded', () => {
|
||||
test('defaults to true', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
expect(frame.expanded).toBe(true)
|
||||
})
|
||||
|
||||
test('can collapse', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.expanded = false
|
||||
expect(frame.expanded).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('findAllWithCriteria', () => {
|
||||
test('filters by type', () => {
|
||||
const api = createAPI()
|
||||
api.createFrame()
|
||||
api.createRectangle()
|
||||
api.createText()
|
||||
api.createRectangle()
|
||||
const rects = api.currentPage.findAllWithCriteria({ types: ['RECTANGLE'] })
|
||||
expect(rects.length).toBe(2)
|
||||
expect(rects.every((n) => n.type === 'RECTANGLE')).toBe(true)
|
||||
})
|
||||
|
||||
test('filters by multiple types', () => {
|
||||
const api = createAPI()
|
||||
api.createFrame()
|
||||
api.createRectangle()
|
||||
api.createText()
|
||||
const result = api.currentPage.findAllWithCriteria({ types: ['FRAME', 'TEXT'] })
|
||||
expect(result.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createComponentFromNode', () => {
|
||||
test('converts frame to component', () => {
|
||||
const api = createAPI()
|
||||
const frame = api.createFrame()
|
||||
frame.name = 'MyButton'
|
||||
frame.resize(200, 50)
|
||||
const child = api.createRectangle()
|
||||
child.name = 'Background'
|
||||
frame.appendChild(child)
|
||||
const frameId = frame.id
|
||||
|
||||
const comp = api.createComponentFromNode(frame)
|
||||
expect(comp.type).toBe('COMPONENT')
|
||||
expect(comp.name).toBe('MyButton')
|
||||
expect(comp.width).toBe(200)
|
||||
expect(comp.height).toBe(50)
|
||||
expect(comp.children.length).toBe(1)
|
||||
expect(comp.children[0].name).toBe('Background')
|
||||
expect(api.getNodeById(frameId)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('variables', () => {
|
||||
test('getLocalVariables returns empty by default', () => {
|
||||
const api = createAPI()
|
||||
expect(api.getLocalVariables()).toEqual([])
|
||||
})
|
||||
|
||||
test('getLocalVariables returns added variables', () => {
|
||||
const api = createAPI()
|
||||
api.graph.addCollection({
|
||||
id: 'col1',
|
||||
name: 'Colors',
|
||||
modes: [{ modeId: 'mode1', name: 'Default' }],
|
||||
defaultModeId: 'mode1',
|
||||
variableIds: [],
|
||||
})
|
||||
api.graph.addVariable({
|
||||
id: 'var1',
|
||||
name: 'primary',
|
||||
type: 'COLOR',
|
||||
collectionId: 'col1',
|
||||
valuesByMode: { mode1: { r: 1, g: 0, b: 0, a: 1 } },
|
||||
description: '',
|
||||
hiddenFromPublishing: false,
|
||||
})
|
||||
expect(api.getLocalVariables().length).toBe(1)
|
||||
expect(api.getLocalVariables('COLOR').length).toBe(1)
|
||||
expect(api.getLocalVariables('FLOAT').length).toBe(0)
|
||||
})
|
||||
|
||||
test('getVariableById', () => {
|
||||
const api = createAPI()
|
||||
api.graph.addCollection({
|
||||
id: 'col1',
|
||||
name: 'Spacing',
|
||||
modes: [{ modeId: 'mode1', name: 'Default' }],
|
||||
defaultModeId: 'mode1',
|
||||
variableIds: [],
|
||||
})
|
||||
api.graph.addVariable({
|
||||
id: 'var1',
|
||||
name: 'spacing-sm',
|
||||
type: 'FLOAT',
|
||||
collectionId: 'col1',
|
||||
valuesByMode: { mode1: 8 },
|
||||
description: '',
|
||||
hiddenFromPublishing: false,
|
||||
})
|
||||
const v = api.getVariableById('var1')
|
||||
expect(v).not.toBeNull()
|
||||
expect(v!.name).toBe('spacing-sm')
|
||||
expect(api.getVariableById('nonexistent')).toBeNull()
|
||||
})
|
||||
|
||||
test('getLocalVariableCollections', () => {
|
||||
const api = createAPI()
|
||||
api.graph.addCollection({
|
||||
id: 'col1',
|
||||
name: 'Colors',
|
||||
modes: [{ modeId: 'mode1', name: 'Default' }],
|
||||
defaultModeId: 'mode1',
|
||||
variableIds: [],
|
||||
})
|
||||
const cols = api.getLocalVariableCollections()
|
||||
expect(cols.length).toBe(1)
|
||||
expect(cols[0].name).toBe('Colors')
|
||||
})
|
||||
|
||||
test('getVariableCollectionById', () => {
|
||||
const api = createAPI()
|
||||
api.graph.addCollection({
|
||||
id: 'col1',
|
||||
name: 'Colors',
|
||||
modes: [{ modeId: 'mode1', name: 'Default' }],
|
||||
defaultModeId: 'mode1',
|
||||
variableIds: [],
|
||||
})
|
||||
expect(api.getVariableCollectionById('col1')?.name).toBe('Colors')
|
||||
expect(api.getVariableCollectionById('nonexistent')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue