@planningcenter/tapestry-migration-cli 2.3.0-rc.4 → 2.3.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/button/index.ts +3 -1
- package/src/components/button/transforms/themeVariantToKind.test.ts +401 -0
- package/src/components/button/transforms/themeVariantToKind.ts +90 -0
- package/src/components/shared/actions/addAttribute.test.ts +300 -0
- package/src/components/shared/actions/addAttribute.ts +65 -0
- package/src/components/shared/actions/addComment.test.ts +1 -1
- package/src/components/shared/actions/getAttributeValue.test.ts +261 -0
- package/src/components/shared/actions/getAttributeValue.ts +15 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.test.ts +374 -0
- package/src/components/shared/transformFactories/attributeCombineFactory.ts +300 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
BooleanLiteral,
|
|
3
|
+
ConditionalExpression,
|
|
4
|
+
Identifier,
|
|
5
|
+
JSXAttribute,
|
|
6
|
+
JSXElement,
|
|
7
|
+
JSXExpressionContainer,
|
|
8
|
+
JSXIdentifier,
|
|
9
|
+
StringLiteral,
|
|
10
|
+
} from "jscodeshift"
|
|
11
|
+
import { describe, expect, it } from "vitest"
|
|
12
|
+
|
|
13
|
+
import { addAttribute, Conditional } from "./addAttribute"
|
|
14
|
+
|
|
15
|
+
const j = jscodeshift.withParser("tsx")
|
|
16
|
+
|
|
17
|
+
function createElementFromCode(code: string): JSXElement {
|
|
18
|
+
const source = j(`<div>${code}</div>`)
|
|
19
|
+
return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getAttributeFromElement(
|
|
23
|
+
element: JSXElement,
|
|
24
|
+
name: string
|
|
25
|
+
): JSXAttribute | null {
|
|
26
|
+
const attributes = element.openingElement.attributes || []
|
|
27
|
+
return (
|
|
28
|
+
(attributes.find(
|
|
29
|
+
(attr) =>
|
|
30
|
+
attr.type === "JSXAttribute" &&
|
|
31
|
+
(attr.name as JSXIdentifier)?.name === name
|
|
32
|
+
) as JSXAttribute) || null
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("addAttribute", () => {
|
|
37
|
+
describe("string values", () => {
|
|
38
|
+
it("should add string attribute to element", () => {
|
|
39
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
40
|
+
|
|
41
|
+
addAttribute({ element, j, name: "kind", value: "primary" })
|
|
42
|
+
|
|
43
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
44
|
+
expect(kindAttr).not.toBeNull()
|
|
45
|
+
expect(kindAttr?.value?.type).toBe("StringLiteral")
|
|
46
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should add multiple string attributes", () => {
|
|
50
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
51
|
+
|
|
52
|
+
addAttribute({ element, j, name: "kind", value: "primary" })
|
|
53
|
+
addAttribute({ element, j, name: "size", value: "large" })
|
|
54
|
+
|
|
55
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
56
|
+
const sizeAttr = getAttributeFromElement(element, "size")
|
|
57
|
+
|
|
58
|
+
expect(kindAttr).not.toBeNull()
|
|
59
|
+
expect(sizeAttr).not.toBeNull()
|
|
60
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
61
|
+
expect((sizeAttr?.value as StringLiteral)?.value).toBe("large")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should handle empty string values", () => {
|
|
65
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
66
|
+
|
|
67
|
+
addAttribute({ element, j, name: "title", value: "" })
|
|
68
|
+
|
|
69
|
+
const titleAttr = getAttributeFromElement(element, "title")
|
|
70
|
+
expect(titleAttr).not.toBeNull()
|
|
71
|
+
expect(titleAttr?.value?.type).toBe("StringLiteral")
|
|
72
|
+
expect((titleAttr?.value as StringLiteral)?.value).toBe("")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("should handle special characters in string values", () => {
|
|
76
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
77
|
+
|
|
78
|
+
addAttribute({ element, j, name: "aria-label", value: "Save & Continue" })
|
|
79
|
+
|
|
80
|
+
const ariaAttr = getAttributeFromElement(element, "aria-label")
|
|
81
|
+
expect(ariaAttr).not.toBeNull()
|
|
82
|
+
expect((ariaAttr?.value as StringLiteral)?.value).toBe("Save & Continue")
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("boolean values", () => {
|
|
87
|
+
it("should add boolean true attribute", () => {
|
|
88
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
89
|
+
|
|
90
|
+
addAttribute({ element, j, name: "disabled", value: true })
|
|
91
|
+
|
|
92
|
+
const disabledAttr = getAttributeFromElement(element, "disabled")
|
|
93
|
+
expect(disabledAttr).not.toBeNull()
|
|
94
|
+
expect(disabledAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
95
|
+
|
|
96
|
+
const container = disabledAttr?.value as JSXExpressionContainer
|
|
97
|
+
expect(container.expression.type).toBe("BooleanLiteral")
|
|
98
|
+
expect((container.expression as BooleanLiteral).value).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should add boolean false attribute", () => {
|
|
102
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
103
|
+
|
|
104
|
+
addAttribute({ element, j, name: "loading", value: false })
|
|
105
|
+
|
|
106
|
+
const loadingAttr = getAttributeFromElement(element, "loading")
|
|
107
|
+
expect(loadingAttr).not.toBeNull()
|
|
108
|
+
expect(loadingAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
109
|
+
|
|
110
|
+
const container = loadingAttr?.value as JSXExpressionContainer
|
|
111
|
+
expect(container.expression.type).toBe("BooleanLiteral")
|
|
112
|
+
expect((container.expression as BooleanLiteral).value).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe("null values", () => {
|
|
117
|
+
it("should not add attribute when value is null", () => {
|
|
118
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
119
|
+
const initialAttrCount = (element.openingElement.attributes || []).length
|
|
120
|
+
|
|
121
|
+
addAttribute({ element, j, name: "kind", value: null })
|
|
122
|
+
|
|
123
|
+
const finalAttrCount = (element.openingElement.attributes || []).length
|
|
124
|
+
expect(finalAttrCount).toBe(initialAttrCount)
|
|
125
|
+
|
|
126
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
127
|
+
expect(kindAttr).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("conditional values", () => {
|
|
132
|
+
it("should add conditional attribute with test, consequent, and alternate", () => {
|
|
133
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
134
|
+
const conditionalValue: Conditional = {
|
|
135
|
+
alternate: "secondary",
|
|
136
|
+
consequent: "primary",
|
|
137
|
+
test: "isPrimary",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
addAttribute({ element, j, name: "kind", value: conditionalValue })
|
|
141
|
+
|
|
142
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
143
|
+
expect(kindAttr).not.toBeNull()
|
|
144
|
+
expect(kindAttr?.value?.type).toBe("JSXExpressionContainer")
|
|
145
|
+
|
|
146
|
+
const container = kindAttr?.value as JSXExpressionContainer
|
|
147
|
+
expect(container.expression.type).toBe("ConditionalExpression")
|
|
148
|
+
|
|
149
|
+
const conditional = container.expression as ConditionalExpression
|
|
150
|
+
expect(conditional.test.type).toBe("Identifier")
|
|
151
|
+
expect((conditional.test as Identifier).name).toBe("isPrimary")
|
|
152
|
+
expect(conditional.consequent.type).toBe("StringLiteral")
|
|
153
|
+
expect((conditional.consequent as StringLiteral).value).toBe("primary")
|
|
154
|
+
expect(conditional.alternate.type).toBe("StringLiteral")
|
|
155
|
+
expect((conditional.alternate as StringLiteral).value).toBe("secondary")
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should handle conditional with empty string values", () => {
|
|
159
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
160
|
+
const conditionalValue: Conditional = {
|
|
161
|
+
alternate: "",
|
|
162
|
+
consequent: "primary",
|
|
163
|
+
test: "hasTheme",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
addAttribute({ element, j, name: "kind", value: conditionalValue })
|
|
167
|
+
|
|
168
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
169
|
+
expect(kindAttr).not.toBeNull()
|
|
170
|
+
|
|
171
|
+
const container = kindAttr?.value as JSXExpressionContainer
|
|
172
|
+
const conditional = container.expression as ConditionalExpression
|
|
173
|
+
expect((conditional.consequent as StringLiteral).value).toBe("primary")
|
|
174
|
+
expect((conditional.alternate as StringLiteral).value).toBe("")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("should handle conditional with same consequent and alternate", () => {
|
|
178
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
179
|
+
const conditionalValue: Conditional = {
|
|
180
|
+
alternate: "neutral",
|
|
181
|
+
consequent: "neutral",
|
|
182
|
+
test: "condition",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
addAttribute({ element, j, name: "kind", value: conditionalValue })
|
|
186
|
+
|
|
187
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
188
|
+
expect(kindAttr).not.toBeNull()
|
|
189
|
+
|
|
190
|
+
const container = kindAttr?.value as JSXExpressionContainer
|
|
191
|
+
const conditional = container.expression as ConditionalExpression
|
|
192
|
+
expect((conditional.consequent as StringLiteral).value).toBe("neutral")
|
|
193
|
+
expect((conditional.alternate as StringLiteral).value).toBe("neutral")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should render conditional attribute correctly in source", () => {
|
|
197
|
+
const source = j("<Button>Save</Button>")
|
|
198
|
+
const element = source.find(j.JSXElement).at(0).get().value as JSXElement
|
|
199
|
+
const conditionalValue: Conditional = {
|
|
200
|
+
alternate: "secondary",
|
|
201
|
+
consequent: "primary",
|
|
202
|
+
test: "isPrimary",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
addAttribute({ element, j, name: "kind", value: conditionalValue })
|
|
206
|
+
|
|
207
|
+
const result = source.toSource()
|
|
208
|
+
expect(result).toContain('kind={isPrimary ? "primary" : "secondary"}')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("should handle multiple conditional attributes", () => {
|
|
212
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
213
|
+
const kindConditional: Conditional = {
|
|
214
|
+
alternate: "secondary",
|
|
215
|
+
consequent: "primary",
|
|
216
|
+
test: "isPrimary",
|
|
217
|
+
}
|
|
218
|
+
const sizeConditional: Conditional = {
|
|
219
|
+
alternate: "small",
|
|
220
|
+
consequent: "large",
|
|
221
|
+
test: "isLarge",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
addAttribute({ element, j, name: "kind", value: kindConditional })
|
|
225
|
+
addAttribute({ element, j, name: "size", value: sizeConditional })
|
|
226
|
+
|
|
227
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
228
|
+
const sizeAttr = getAttributeFromElement(element, "size")
|
|
229
|
+
|
|
230
|
+
expect(kindAttr).not.toBeNull()
|
|
231
|
+
expect(sizeAttr).not.toBeNull()
|
|
232
|
+
expect(element.openingElement.attributes).toHaveLength(2)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe("adding to elements with existing attributes", () => {
|
|
237
|
+
it("should add attribute to element that already has attributes", () => {
|
|
238
|
+
const element = createElementFromCode(
|
|
239
|
+
'<Button onClick={handleClick} className="btn">Save</Button>'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
addAttribute({ element, j, name: "kind", value: "primary" })
|
|
243
|
+
|
|
244
|
+
expect(element.openingElement.attributes).toHaveLength(3)
|
|
245
|
+
|
|
246
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
247
|
+
expect(kindAttr).not.toBeNull()
|
|
248
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
249
|
+
|
|
250
|
+
const onClickAttr = getAttributeFromElement(element, "onClick")
|
|
251
|
+
const classAttr = getAttributeFromElement(element, "className")
|
|
252
|
+
expect(onClickAttr).not.toBeNull()
|
|
253
|
+
expect(classAttr).not.toBeNull()
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe("adding to self-closing elements", () => {
|
|
258
|
+
it("should add attribute to self-closing element", () => {
|
|
259
|
+
const element = createElementFromCode("<Button />")
|
|
260
|
+
|
|
261
|
+
addAttribute({ element, j, name: "kind", value: "primary" })
|
|
262
|
+
|
|
263
|
+
const kindAttr = getAttributeFromElement(element, "kind")
|
|
264
|
+
expect(kindAttr).not.toBeNull()
|
|
265
|
+
expect((kindAttr?.value as StringLiteral)?.value).toBe("primary")
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe("integration with JSCodeshift", () => {
|
|
270
|
+
it("should render correctly in transformed source", () => {
|
|
271
|
+
const source = j("<Button>Save</Button>")
|
|
272
|
+
const element = source.find(j.JSXElement).at(0).get().value as JSXElement
|
|
273
|
+
|
|
274
|
+
addAttribute({ element, j, name: "kind", value: "primary" })
|
|
275
|
+
addAttribute({ element, j, name: "disabled", value: true })
|
|
276
|
+
|
|
277
|
+
const result = source.toSource()
|
|
278
|
+
|
|
279
|
+
expect(result).toContain('kind="primary"')
|
|
280
|
+
expect(result).toContain("disabled={true}")
|
|
281
|
+
expect(result).toContain("<Button")
|
|
282
|
+
expect(result).toContain(">Save</Button>")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("should handle complex attribute names", () => {
|
|
286
|
+
const element = createElementFromCode("<Button>Save</Button>")
|
|
287
|
+
|
|
288
|
+
addAttribute({ element, j, name: "data-testid", value: "save-button" })
|
|
289
|
+
addAttribute({ element, j, name: "aria-describedby", value: "help-text" })
|
|
290
|
+
|
|
291
|
+
const testIdAttr = getAttributeFromElement(element, "data-testid")
|
|
292
|
+
const ariaAttr = getAttributeFromElement(element, "aria-describedby")
|
|
293
|
+
|
|
294
|
+
expect(testIdAttr).not.toBeNull()
|
|
295
|
+
expect(ariaAttr).not.toBeNull()
|
|
296
|
+
expect((testIdAttr?.value as StringLiteral)?.value).toBe("save-button")
|
|
297
|
+
expect((ariaAttr?.value as StringLiteral)?.value).toBe("help-text")
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { JSCodeshift, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export interface Conditional {
|
|
4
|
+
alternate: string
|
|
5
|
+
consequent: string
|
|
6
|
+
test: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatValue(value: string | boolean, j: JSCodeshift) {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
return j.stringLiteral(value)
|
|
12
|
+
} else if (typeof value === "boolean") {
|
|
13
|
+
return j.jsxExpressionContainer(j.booleanLiteral(value))
|
|
14
|
+
} else {
|
|
15
|
+
throw new Error(`Unsupported attribute value type: ${typeof value}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addAttribute({
|
|
20
|
+
element,
|
|
21
|
+
name,
|
|
22
|
+
j,
|
|
23
|
+
value,
|
|
24
|
+
}: {
|
|
25
|
+
element: JSXElement
|
|
26
|
+
j: JSCodeshift
|
|
27
|
+
name: string
|
|
28
|
+
value: string | boolean | Conditional | null
|
|
29
|
+
}) {
|
|
30
|
+
if (value === null) return
|
|
31
|
+
const attributes = element.openingElement.attributes || []
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
typeof value === "object" &&
|
|
35
|
+
"test" in value &&
|
|
36
|
+
"consequent" in value &&
|
|
37
|
+
"alternate" in value
|
|
38
|
+
) {
|
|
39
|
+
addConditionalAttribute(value, element, j, name)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const formattedValue = formatValue(value, j)
|
|
44
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier(name), formattedValue))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function addConditionalAttribute(
|
|
48
|
+
{ test, consequent, alternate }: Conditional,
|
|
49
|
+
element: JSXElement,
|
|
50
|
+
j: JSCodeshift,
|
|
51
|
+
targetAttribute: string
|
|
52
|
+
) {
|
|
53
|
+
const conditional = j.jsxExpressionContainer(
|
|
54
|
+
j.conditionalExpression(
|
|
55
|
+
j.identifier(test),
|
|
56
|
+
j.stringLiteral(consequent),
|
|
57
|
+
j.stringLiteral(alternate)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
const attribute = j.jsxAttribute(
|
|
61
|
+
j.jsxIdentifier(targetAttribute),
|
|
62
|
+
conditional
|
|
63
|
+
)
|
|
64
|
+
element.openingElement.attributes?.push(attribute)
|
|
65
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import jscodeshift, {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXIdentifier,
|
|
5
|
+
} from "jscodeshift"
|
|
6
|
+
import { describe, expect, it } from "vitest"
|
|
7
|
+
|
|
8
|
+
import { getAttributeValue } from "./getAttributeValue"
|
|
9
|
+
|
|
10
|
+
const j = jscodeshift.withParser("tsx")
|
|
11
|
+
|
|
12
|
+
function createElementFromCode(code: string): JSXElement {
|
|
13
|
+
const source = j(`<div>${code}</div>`)
|
|
14
|
+
return source.find(j.JSXElement).at(0).get().value.children?.[0] as JSXElement
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getAttributeFromElement(
|
|
18
|
+
element: JSXElement,
|
|
19
|
+
name: string
|
|
20
|
+
): JSXAttribute | null {
|
|
21
|
+
const attributes = element.openingElement.attributes || []
|
|
22
|
+
return (
|
|
23
|
+
(attributes.find(
|
|
24
|
+
(attr) =>
|
|
25
|
+
attr.type === "JSXAttribute" &&
|
|
26
|
+
(attr.name as JSXIdentifier)?.name === name
|
|
27
|
+
) as JSXAttribute) || null
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("getAttributeValue", () => {
|
|
32
|
+
describe("string literal values", () => {
|
|
33
|
+
it("should extract string literal value", () => {
|
|
34
|
+
const element = createElementFromCode(
|
|
35
|
+
'<Button kind="primary">Save</Button>'
|
|
36
|
+
)
|
|
37
|
+
const kindAttribute = getAttributeFromElement(element, "kind")
|
|
38
|
+
|
|
39
|
+
const value = getAttributeValue({ attribute: kindAttribute, j })
|
|
40
|
+
|
|
41
|
+
expect(value).toBe("primary")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("should handle empty string literal", () => {
|
|
45
|
+
const element = createElementFromCode('<Button title="">Save</Button>')
|
|
46
|
+
const titleAttribute = getAttributeFromElement(element, "title")
|
|
47
|
+
|
|
48
|
+
const value = getAttributeValue({ attribute: titleAttribute, j })
|
|
49
|
+
|
|
50
|
+
expect(value).toBe("")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("should handle strings with special characters", () => {
|
|
54
|
+
const element = createElementFromCode(
|
|
55
|
+
'<Button aria-label="Save & Continue">Save</Button>'
|
|
56
|
+
)
|
|
57
|
+
const ariaAttribute = getAttributeFromElement(element, "aria-label")
|
|
58
|
+
|
|
59
|
+
const value = getAttributeValue({ attribute: ariaAttribute, j })
|
|
60
|
+
|
|
61
|
+
expect(value).toBe("Save & Continue")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should handle strings with quotes", () => {
|
|
65
|
+
const element = createElementFromCode(
|
|
66
|
+
"<Button title='Say \"Hello\"'>Save</Button>"
|
|
67
|
+
)
|
|
68
|
+
const titleAttribute = getAttributeFromElement(element, "title")
|
|
69
|
+
|
|
70
|
+
const value = getAttributeValue({ attribute: titleAttribute, j })
|
|
71
|
+
|
|
72
|
+
expect(value).toBe('Say "Hello"')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("expression container values", () => {
|
|
77
|
+
it("should convert boolean expression to source code", () => {
|
|
78
|
+
const element = createElementFromCode(
|
|
79
|
+
"<Button disabled={true}>Save</Button>"
|
|
80
|
+
)
|
|
81
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
82
|
+
|
|
83
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
84
|
+
|
|
85
|
+
expect(value).toBe("{true}")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should convert variable expression to source code", () => {
|
|
89
|
+
const element = createElementFromCode(
|
|
90
|
+
"<Button onClick={handleClick}>Save</Button>"
|
|
91
|
+
)
|
|
92
|
+
const onClickAttribute = getAttributeFromElement(element, "onClick")
|
|
93
|
+
|
|
94
|
+
const value = getAttributeValue({ attribute: onClickAttribute, j })
|
|
95
|
+
|
|
96
|
+
expect(value).toBe("{handleClick}")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("should convert complex expression to source code", () => {
|
|
100
|
+
const element = createElementFromCode(
|
|
101
|
+
'<Button style={{ color: "red", fontSize: 16 }}>Save</Button>'
|
|
102
|
+
)
|
|
103
|
+
const styleAttribute = getAttributeFromElement(element, "style")
|
|
104
|
+
|
|
105
|
+
const value = getAttributeValue({ attribute: styleAttribute, j })
|
|
106
|
+
|
|
107
|
+
expect(value).toContain("color")
|
|
108
|
+
expect(value).toContain("red")
|
|
109
|
+
expect(value).toContain("fontSize")
|
|
110
|
+
expect(value).toContain("16")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("should handle function call expressions", () => {
|
|
114
|
+
const element = createElementFromCode(
|
|
115
|
+
'<Button className={getButtonClass("primary")}>Save</Button>'
|
|
116
|
+
)
|
|
117
|
+
const classAttribute = getAttributeFromElement(element, "className")
|
|
118
|
+
|
|
119
|
+
const value = getAttributeValue({ attribute: classAttribute, j })
|
|
120
|
+
|
|
121
|
+
expect(value).toBe('{getButtonClass("primary")}')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("should handle template literal expressions", () => {
|
|
125
|
+
const element = createElementFromCode(
|
|
126
|
+
"<Button className={`btn-${variant}`}>Save</Button>"
|
|
127
|
+
)
|
|
128
|
+
const classAttribute = getAttributeFromElement(element, "className")
|
|
129
|
+
|
|
130
|
+
const value = getAttributeValue({ attribute: classAttribute, j })
|
|
131
|
+
|
|
132
|
+
expect(value).toBe("{`btn-${variant}`}")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("should handle ternary expressions", () => {
|
|
136
|
+
const element = createElementFromCode(
|
|
137
|
+
"<Button disabled={loading ? true : false}>Save</Button>"
|
|
138
|
+
)
|
|
139
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
140
|
+
|
|
141
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
142
|
+
|
|
143
|
+
expect(value).toBe("{loading ? true : false}")
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("null and undefined attributes", () => {
|
|
148
|
+
it("should return null for null attribute", () => {
|
|
149
|
+
const value = getAttributeValue({ attribute: null, j })
|
|
150
|
+
|
|
151
|
+
expect(value).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should return null for undefined attribute", () => {
|
|
155
|
+
const value = getAttributeValue({ attribute: undefined, j })
|
|
156
|
+
|
|
157
|
+
expect(value).toBeNull()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("attributes without values", () => {
|
|
162
|
+
it("should return null for boolean attribute without value", () => {
|
|
163
|
+
const element = createElementFromCode("<Button disabled>Save</Button>")
|
|
164
|
+
const disabledAttribute = getAttributeFromElement(element, "disabled")
|
|
165
|
+
|
|
166
|
+
const value = getAttributeValue({ attribute: disabledAttribute, j })
|
|
167
|
+
|
|
168
|
+
expect(value).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("should handle attribute with null value", () => {
|
|
172
|
+
const mockAttribute = {
|
|
173
|
+
name: { name: "test", type: "JSXIdentifier" },
|
|
174
|
+
type: "JSXAttribute",
|
|
175
|
+
value: null,
|
|
176
|
+
} as JSXAttribute
|
|
177
|
+
|
|
178
|
+
const value = getAttributeValue({ attribute: mockAttribute, j })
|
|
179
|
+
|
|
180
|
+
expect(value).toBeNull()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe("edge cases", () => {
|
|
185
|
+
it("should handle numeric expressions", () => {
|
|
186
|
+
const element = createElementFromCode(
|
|
187
|
+
"<Button tabIndex={0}>Save</Button>"
|
|
188
|
+
)
|
|
189
|
+
const tabIndexAttribute = getAttributeFromElement(element, "tabIndex")
|
|
190
|
+
|
|
191
|
+
const value = getAttributeValue({ attribute: tabIndexAttribute, j })
|
|
192
|
+
|
|
193
|
+
expect(value).toBe("{0}")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should handle array expressions", () => {
|
|
197
|
+
const element = createElementFromCode(
|
|
198
|
+
"<Button data={[1, 2, 3]}>Save</Button>"
|
|
199
|
+
)
|
|
200
|
+
const dataAttribute = getAttributeFromElement(element, "data")
|
|
201
|
+
|
|
202
|
+
const value = getAttributeValue({ attribute: dataAttribute, j })
|
|
203
|
+
|
|
204
|
+
expect(value).toBe("{[1, 2, 3]}")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("should handle nested object expressions", () => {
|
|
208
|
+
const element = createElementFromCode(
|
|
209
|
+
'<Button config={{ theme: { primary: "blue" } }}>Save</Button>'
|
|
210
|
+
)
|
|
211
|
+
const configAttribute = getAttributeFromElement(element, "config")
|
|
212
|
+
|
|
213
|
+
const value = getAttributeValue({ attribute: configAttribute, j })
|
|
214
|
+
|
|
215
|
+
expect(value).toContain("theme")
|
|
216
|
+
expect(value).toContain("primary")
|
|
217
|
+
expect(value).toContain("blue")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("should handle JSX element expressions", () => {
|
|
221
|
+
const element = createElementFromCode(
|
|
222
|
+
'<Button icon={<Icon name="save" />}>Save</Button>'
|
|
223
|
+
)
|
|
224
|
+
const iconAttribute = getAttributeFromElement(element, "icon")
|
|
225
|
+
|
|
226
|
+
const value = getAttributeValue({ attribute: iconAttribute, j })
|
|
227
|
+
|
|
228
|
+
expect(value).toBe('{<Icon name="save" />}')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("integration scenarios", () => {
|
|
233
|
+
it("should handle mixed attribute types on same element", () => {
|
|
234
|
+
const element = createElementFromCode(
|
|
235
|
+
'<Button kind="primary" disabled={true} onClick={handleClick} data-testid="save-btn">Save</Button>'
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const kindValue = getAttributeValue({
|
|
239
|
+
attribute: getAttributeFromElement(element, "kind"),
|
|
240
|
+
j,
|
|
241
|
+
})
|
|
242
|
+
const disabledValue = getAttributeValue({
|
|
243
|
+
attribute: getAttributeFromElement(element, "disabled"),
|
|
244
|
+
j,
|
|
245
|
+
})
|
|
246
|
+
const onClickValue = getAttributeValue({
|
|
247
|
+
attribute: getAttributeFromElement(element, "onClick"),
|
|
248
|
+
j,
|
|
249
|
+
})
|
|
250
|
+
const testIdValue = getAttributeValue({
|
|
251
|
+
attribute: getAttributeFromElement(element, "data-testid"),
|
|
252
|
+
j,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(kindValue).toBe("primary")
|
|
256
|
+
expect(disabledValue).toBe("{true}")
|
|
257
|
+
expect(onClickValue).toBe("{handleClick}")
|
|
258
|
+
expect(testIdValue).toBe("save-btn")
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JSCodeshift, JSXAttribute } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
export function getAttributeValue({
|
|
4
|
+
attribute,
|
|
5
|
+
j,
|
|
6
|
+
}: {
|
|
7
|
+
attribute: JSXAttribute | null | undefined
|
|
8
|
+
j: JSCodeshift
|
|
9
|
+
}) {
|
|
10
|
+
return attribute && attribute.value
|
|
11
|
+
? attribute?.value?.type === "StringLiteral"
|
|
12
|
+
? attribute.value.value
|
|
13
|
+
: j(attribute.value).toSource()
|
|
14
|
+
: null
|
|
15
|
+
}
|