@planningcenter/tapestry-migration-cli 3.1.0-rc.7 → 3.1.0-rc.9
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 +3 -3
- package/src/components/input/transforms/removeTypeInput.test.ts +212 -0
- package/src/components/input/transforms/removeTypeInput.ts +22 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +33 -0
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +2 -1
- package/src/components/text-area/index.ts +48 -0
- package/src/components/text-area/transforms/auditSpreadProps.test.ts +139 -0
- package/src/components/text-area/transforms/auditSpreadProps.ts +10 -0
- package/src/components/text-area/transforms/convertStyleProps.test.ts +158 -0
- package/src/components/text-area/transforms/convertStyleProps.ts +10 -0
- package/src/components/text-area/transforms/innerRefToRef.test.ts +206 -0
- package/src/components/text-area/transforms/innerRefToRef.ts +14 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.test.ts +477 -0
- package/src/components/text-area/transforms/mergeFieldIntoTextArea.ts +227 -0
- package/src/components/text-area/transforms/moveTextAreaImport.test.ts +168 -0
- package/src/components/text-area/transforms/moveTextAreaImport.ts +13 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.test.ts +129 -0
- package/src/components/text-area/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.test.ts +183 -0
- package/src/components/text-area/transforms/removeRedundantAriaLabel.ts +59 -0
- package/src/components/text-area/transforms/sizeMapping.test.ts +199 -0
- package/src/components/text-area/transforms/sizeMapping.ts +15 -0
- package/src/components/text-area/transforms/stateToInvalid.test.ts +204 -0
- package/src/components/text-area/transforms/stateToInvalid.ts +57 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.test.ts +133 -0
- package/src/components/text-area/transforms/stateToInvalidTernary.ts +11 -0
- package/src/components/text-area/transforms/unsupportedProps.test.ts +275 -0
- package/src/components/text-area/transforms/unsupportedProps.ts +35 -0
- package/src/index.ts +3 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { JSXElement, JSXText, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
4
|
+
import {
|
|
5
|
+
getImportName,
|
|
6
|
+
removeImportFromDeclaration,
|
|
7
|
+
} from "../../shared/transformFactories/helpers/manageImports"
|
|
8
|
+
|
|
9
|
+
const SCOPE = "mergeFieldIntoTextArea"
|
|
10
|
+
|
|
11
|
+
const transform: Transform = (fileInfo, api) => {
|
|
12
|
+
const j = api.jscodeshift
|
|
13
|
+
const source = j(fileInfo.source)
|
|
14
|
+
|
|
15
|
+
const fieldLocalName = getImportName(
|
|
16
|
+
"Field",
|
|
17
|
+
"@planningcenter/tapestry-react",
|
|
18
|
+
{ j, source }
|
|
19
|
+
)
|
|
20
|
+
if (!fieldLocalName) return null
|
|
21
|
+
|
|
22
|
+
const textAreaLocalName = getImportName(
|
|
23
|
+
"TextArea",
|
|
24
|
+
"@planningcenter/tapestry-react",
|
|
25
|
+
{ j, source }
|
|
26
|
+
)
|
|
27
|
+
if (!textAreaLocalName) return null
|
|
28
|
+
|
|
29
|
+
let hasChanges = false
|
|
30
|
+
let anyFieldRemoved = false
|
|
31
|
+
|
|
32
|
+
source.find(j.JSXElement).forEach((path) => {
|
|
33
|
+
const el = path.value
|
|
34
|
+
const opening = el.openingElement
|
|
35
|
+
|
|
36
|
+
if (opening.name.type !== "JSXIdentifier") return
|
|
37
|
+
if (opening.name.name !== fieldLocalName) return
|
|
38
|
+
|
|
39
|
+
const elementChildren = (el.children || []).filter(
|
|
40
|
+
(child) =>
|
|
41
|
+
child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const textAreaChildren = elementChildren.filter((child) => {
|
|
45
|
+
if (child.type !== "JSXElement") return false
|
|
46
|
+
const childOpening = (child as JSXElement).openingElement
|
|
47
|
+
return (
|
|
48
|
+
childOpening.name.type === "JSXIdentifier" &&
|
|
49
|
+
childOpening.name.name === textAreaLocalName
|
|
50
|
+
)
|
|
51
|
+
}) as JSXElement[]
|
|
52
|
+
|
|
53
|
+
// Case: exactly 1 child and it is a TextArea — merge props and unwrap
|
|
54
|
+
if (elementChildren.length === 1 && textAreaChildren.length === 1) {
|
|
55
|
+
const textAreaEl = textAreaChildren[0]
|
|
56
|
+
const fieldAttrs = opening.attributes || []
|
|
57
|
+
|
|
58
|
+
// Bail out if Field has spread props — we can't know what they contain
|
|
59
|
+
const hasFieldSpreads = fieldAttrs.some(
|
|
60
|
+
(attr) => attr.type === "JSXSpreadAttribute"
|
|
61
|
+
)
|
|
62
|
+
if (hasFieldSpreads) {
|
|
63
|
+
addComment({
|
|
64
|
+
element: textAreaEl,
|
|
65
|
+
j,
|
|
66
|
+
scope: SCOPE,
|
|
67
|
+
source,
|
|
68
|
+
text: "Field has spread props that cannot be auto-merged into TextArea. Please migrate manually.",
|
|
69
|
+
})
|
|
70
|
+
hasChanges = true
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const attr of fieldAttrs) {
|
|
75
|
+
if (attr.type !== "JSXAttribute") continue
|
|
76
|
+
if (attr.name.type !== "JSXIdentifier") continue
|
|
77
|
+
|
|
78
|
+
const attrName = attr.name.name
|
|
79
|
+
const textAreaAttrs = textAreaEl.openingElement.attributes || []
|
|
80
|
+
|
|
81
|
+
if (attrName === "label") {
|
|
82
|
+
const hasLabel = textAreaAttrs.some(
|
|
83
|
+
(a) =>
|
|
84
|
+
a.type === "JSXAttribute" &&
|
|
85
|
+
a.name?.type === "JSXIdentifier" &&
|
|
86
|
+
a.name.name === "label"
|
|
87
|
+
)
|
|
88
|
+
if (hasLabel) {
|
|
89
|
+
addComment({
|
|
90
|
+
element: textAreaEl,
|
|
91
|
+
j,
|
|
92
|
+
scope: SCOPE,
|
|
93
|
+
source,
|
|
94
|
+
text: "Field had label prop but TextArea already has label. Please migrate manually.",
|
|
95
|
+
})
|
|
96
|
+
} else {
|
|
97
|
+
textAreaEl.openingElement.attributes.push(attr)
|
|
98
|
+
}
|
|
99
|
+
} else if (attrName === "feedbackText") {
|
|
100
|
+
const hasDescription = textAreaAttrs.some(
|
|
101
|
+
(a) =>
|
|
102
|
+
a.type === "JSXAttribute" &&
|
|
103
|
+
a.name?.type === "JSXIdentifier" &&
|
|
104
|
+
a.name.name === "description"
|
|
105
|
+
)
|
|
106
|
+
if (hasDescription) {
|
|
107
|
+
addComment({
|
|
108
|
+
element: textAreaEl,
|
|
109
|
+
j,
|
|
110
|
+
scope: SCOPE,
|
|
111
|
+
source,
|
|
112
|
+
text: "Field had feedbackText prop but TextArea already has description. Please migrate manually.",
|
|
113
|
+
})
|
|
114
|
+
} else {
|
|
115
|
+
const newAttr = j.jsxAttribute(
|
|
116
|
+
j.jsxIdentifier("description"),
|
|
117
|
+
attr.value
|
|
118
|
+
)
|
|
119
|
+
textAreaEl.openingElement.attributes.push(newAttr)
|
|
120
|
+
}
|
|
121
|
+
} else if (attrName === "state") {
|
|
122
|
+
const hasState = textAreaAttrs.some(
|
|
123
|
+
(a) =>
|
|
124
|
+
a.type === "JSXAttribute" &&
|
|
125
|
+
a.name?.type === "JSXIdentifier" &&
|
|
126
|
+
a.name.name === "state"
|
|
127
|
+
)
|
|
128
|
+
if (hasState) {
|
|
129
|
+
addComment({
|
|
130
|
+
element: textAreaEl,
|
|
131
|
+
j,
|
|
132
|
+
scope: SCOPE,
|
|
133
|
+
source,
|
|
134
|
+
text: "Field had state prop but TextArea already has state. Please migrate manually.",
|
|
135
|
+
})
|
|
136
|
+
} else {
|
|
137
|
+
textAreaEl.openingElement.attributes.push(attr)
|
|
138
|
+
}
|
|
139
|
+
} else if (attrName === "key") {
|
|
140
|
+
const hasKey = textAreaAttrs.some(
|
|
141
|
+
(a) =>
|
|
142
|
+
a.type === "JSXAttribute" &&
|
|
143
|
+
a.name?.type === "JSXIdentifier" &&
|
|
144
|
+
a.name.name === "key"
|
|
145
|
+
)
|
|
146
|
+
if (!hasKey) {
|
|
147
|
+
textAreaEl.openingElement.attributes.push(attr)
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// Unsupported prop — add comment to TextArea
|
|
151
|
+
addComment({
|
|
152
|
+
element: textAreaEl,
|
|
153
|
+
j,
|
|
154
|
+
scope: SCOPE,
|
|
155
|
+
source,
|
|
156
|
+
text: `Field prop '${attrName}' is not supported by TextArea. Please migrate manually.`,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parent = path.parent?.value
|
|
162
|
+
if (parent?.children) {
|
|
163
|
+
const idx = parent.children.indexOf(el)
|
|
164
|
+
if (idx === -1) return
|
|
165
|
+
parent.children.splice(idx, 1, ...(el.children || []))
|
|
166
|
+
} else {
|
|
167
|
+
// Root JSX (e.g. directly inside return parens) — use path.replace
|
|
168
|
+
const nonWsChildren = (el.children || []).filter(
|
|
169
|
+
(child) =>
|
|
170
|
+
child.type !== "JSXText" || (child as JSXText).value.trim() !== ""
|
|
171
|
+
)
|
|
172
|
+
if (nonWsChildren.length === 1) {
|
|
173
|
+
path.replace(nonWsChildren[0])
|
|
174
|
+
} else {
|
|
175
|
+
path.replace(
|
|
176
|
+
j.jsxFragment(
|
|
177
|
+
j.jsxOpeningFragment(),
|
|
178
|
+
j.jsxClosingFragment(),
|
|
179
|
+
el.children || []
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
hasChanges = true
|
|
185
|
+
anyFieldRemoved = true
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Case: more than 1 non-whitespace child — comment each TextArea child, leave Field
|
|
190
|
+
if (elementChildren.length > 1) {
|
|
191
|
+
for (const child of textAreaChildren) {
|
|
192
|
+
addComment({
|
|
193
|
+
element: child,
|
|
194
|
+
j,
|
|
195
|
+
scope: SCOPE,
|
|
196
|
+
source,
|
|
197
|
+
text: "Field has multiple children and cannot be auto-merged into TextArea. Please migrate manually.",
|
|
198
|
+
})
|
|
199
|
+
hasChanges = true
|
|
200
|
+
}
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Case: exactly 1 child but not a TextArea — skip without comment
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Remove Field from imports only if all Field usages were converted
|
|
208
|
+
if (anyFieldRemoved) {
|
|
209
|
+
const stillUsesField =
|
|
210
|
+
source.find(j.JSXOpeningElement, {
|
|
211
|
+
name: { name: fieldLocalName },
|
|
212
|
+
}).length > 0
|
|
213
|
+
|
|
214
|
+
if (!stillUsesField) {
|
|
215
|
+
const fieldImports = source.find(j.ImportDeclaration, {
|
|
216
|
+
source: { value: "@planningcenter/tapestry-react" },
|
|
217
|
+
})
|
|
218
|
+
for (let i = 0; i < fieldImports.length; i++) {
|
|
219
|
+
removeImportFromDeclaration(fieldImports.at(i), "Field")
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return hasChanges ? source.toSource() : null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default transform
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./moveTextAreaImport"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
const result = transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{}
|
|
14
|
+
) as string | null
|
|
15
|
+
return result || source
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("moveTextAreaImport transform", () => {
|
|
19
|
+
describe("import migration", () => {
|
|
20
|
+
it("should change import from tapestry-react to tapestry", () => {
|
|
21
|
+
const input = `
|
|
22
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
return <TextArea label="Notes" />
|
|
26
|
+
}
|
|
27
|
+
`.trim()
|
|
28
|
+
|
|
29
|
+
const result = applyTransform(input)
|
|
30
|
+
expect(result).toContain(
|
|
31
|
+
'import { TextArea } from "@planningcenter/tapestry"'
|
|
32
|
+
)
|
|
33
|
+
expect(result).not.toContain("@planningcenter/tapestry-react")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should only move TextArea, leaving other imports in place", () => {
|
|
37
|
+
const input = `
|
|
38
|
+
import { Button, TextArea } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
function Test() {
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<Button>Click</Button>
|
|
44
|
+
<TextArea label="Notes" />
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
`.trim()
|
|
49
|
+
|
|
50
|
+
const result = applyTransform(input)
|
|
51
|
+
expect(result).toContain(
|
|
52
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
53
|
+
)
|
|
54
|
+
expect(result).toContain(
|
|
55
|
+
'import { TextArea } from "@planningcenter/tapestry"'
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should handle TextArea as sole import in declaration", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
62
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
63
|
+
|
|
64
|
+
function Test() {
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
<Button>Click</Button>
|
|
68
|
+
<TextArea label="Notes" />
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
`.trim()
|
|
73
|
+
|
|
74
|
+
const result = applyTransform(input)
|
|
75
|
+
expect(result).toContain(
|
|
76
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
77
|
+
)
|
|
78
|
+
expect(result).toContain(
|
|
79
|
+
'import { TextArea } from "@planningcenter/tapestry"'
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("should not affect other components", () => {
|
|
84
|
+
const input = `
|
|
85
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
86
|
+
|
|
87
|
+
function Test() {
|
|
88
|
+
return <Button>Click me</Button>
|
|
89
|
+
}
|
|
90
|
+
`.trim()
|
|
91
|
+
|
|
92
|
+
const result = applyTransform(input)
|
|
93
|
+
expect(result).toContain(
|
|
94
|
+
'import { Button } from "@planningcenter/tapestry-react"'
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("edge cases", () => {
|
|
100
|
+
it("should handle already migrated imports", () => {
|
|
101
|
+
const input = `
|
|
102
|
+
import { TextArea } from "@planningcenter/tapestry"
|
|
103
|
+
|
|
104
|
+
function Test() {
|
|
105
|
+
return <TextArea label="Notes" />
|
|
106
|
+
}
|
|
107
|
+
`.trim()
|
|
108
|
+
|
|
109
|
+
const result = applyTransform(input)
|
|
110
|
+
expect(result).toContain(
|
|
111
|
+
'import { TextArea } from "@planningcenter/tapestry"'
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("should handle no imports", () => {
|
|
116
|
+
const input = `
|
|
117
|
+
function Test() {
|
|
118
|
+
return <div>No imports</div>
|
|
119
|
+
}
|
|
120
|
+
`.trim()
|
|
121
|
+
|
|
122
|
+
const result = applyTransform(input)
|
|
123
|
+
expect(result).toBe(input)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should preserve all attributes", () => {
|
|
127
|
+
const input = `
|
|
128
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
129
|
+
|
|
130
|
+
function Test() {
|
|
131
|
+
return (
|
|
132
|
+
<TextArea
|
|
133
|
+
label="Notes"
|
|
134
|
+
placeholder="Enter notes"
|
|
135
|
+
rows={5}
|
|
136
|
+
disabled
|
|
137
|
+
onChange={() => {}}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
`.trim()
|
|
142
|
+
|
|
143
|
+
const result = applyTransform(input)
|
|
144
|
+
expect(result).toContain(
|
|
145
|
+
'import { TextArea } from "@planningcenter/tapestry"'
|
|
146
|
+
)
|
|
147
|
+
expect(result).toContain('label="Notes"')
|
|
148
|
+
expect(result).toContain('placeholder="Enter notes"')
|
|
149
|
+
expect(result).toContain("rows={5}")
|
|
150
|
+
expect(result).toContain("disabled")
|
|
151
|
+
expect(result).toContain("onChange={() => {}}")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("should handle alias import", () => {
|
|
155
|
+
const input = `
|
|
156
|
+
import { TextArea as MyTextArea } from "@planningcenter/tapestry-react"
|
|
157
|
+
|
|
158
|
+
function Test() {
|
|
159
|
+
return <MyTextArea label="Notes" />
|
|
160
|
+
}
|
|
161
|
+
`.trim()
|
|
162
|
+
|
|
163
|
+
const result = applyTransform(input)
|
|
164
|
+
expect(result).toContain("@planningcenter/tapestry")
|
|
165
|
+
expect(result).not.toContain("@planningcenter/tapestry-react")
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { componentTransformFactory } from "../../shared/transformFactories/componentTransformFactory"
|
|
4
|
+
|
|
5
|
+
const transform: Transform = componentTransformFactory({
|
|
6
|
+
condition: () => true,
|
|
7
|
+
fromComponent: "TextArea",
|
|
8
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
9
|
+
toComponent: "TextArea",
|
|
10
|
+
toPackage: "@planningcenter/tapestry",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export default transform
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./removeDuplicateKeys"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string) {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{}
|
|
14
|
+
) as string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("removeDuplicateKeys transform", () => {
|
|
18
|
+
describe("TextArea elements with duplicate attributes", () => {
|
|
19
|
+
it("should remove duplicate attributes from TextArea elements", () => {
|
|
20
|
+
const source = `
|
|
21
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export function TestComponent() {
|
|
24
|
+
return <TextArea label="First" disabled label="Second">Save</TextArea>
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(source)
|
|
29
|
+
|
|
30
|
+
expect(result).not.toBeNull()
|
|
31
|
+
expect(result).toContain('label="First"')
|
|
32
|
+
expect(result).not.toContain('label="Second"')
|
|
33
|
+
expect(result).toContain("disabled")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("should preserve first occurrence of duplicate attributes", () => {
|
|
37
|
+
const source = `
|
|
38
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
39
|
+
|
|
40
|
+
export function TestComponent() {
|
|
41
|
+
return <TextArea onClick={handleFirst} aria-label="First" onClick={handleSecond} aria-label="Second" />
|
|
42
|
+
}
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
const result = applyTransform(source)
|
|
46
|
+
|
|
47
|
+
expect(result).not.toBeNull()
|
|
48
|
+
expect(result).toContain("onClick={handleFirst}")
|
|
49
|
+
expect(result).not.toContain("onClick={handleSecond}")
|
|
50
|
+
expect(result).toContain('aria-label="First"')
|
|
51
|
+
expect(result).not.toContain('aria-label="Second"')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("TextArea elements without duplicate attributes", () => {
|
|
56
|
+
it("should return null when no duplicates exist", () => {
|
|
57
|
+
const source = `
|
|
58
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
59
|
+
|
|
60
|
+
export function TestComponent() {
|
|
61
|
+
return <TextArea label="Name" disabled />
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const result = applyTransform(source)
|
|
66
|
+
|
|
67
|
+
expect(result).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("should return null for TextArea with no attributes", () => {
|
|
71
|
+
const source = `
|
|
72
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
73
|
+
|
|
74
|
+
export function TestComponent() {
|
|
75
|
+
return <TextArea />
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
const result = applyTransform(source)
|
|
80
|
+
|
|
81
|
+
expect(result).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("import handling", () => {
|
|
86
|
+
it("should only process files that import TextArea from tapestry-react", () => {
|
|
87
|
+
const source = `
|
|
88
|
+
import { TextArea } from "some-other-library"
|
|
89
|
+
|
|
90
|
+
export function TestComponent() {
|
|
91
|
+
return <TextArea label="First" label="Second" />
|
|
92
|
+
}
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
const result = applyTransform(source)
|
|
96
|
+
|
|
97
|
+
expect(result).toBeNull()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe("mixed elements", () => {
|
|
102
|
+
it("should only process TextArea elements, not other elements", () => {
|
|
103
|
+
const source = `
|
|
104
|
+
import { TextArea } from "@planningcenter/tapestry-react"
|
|
105
|
+
|
|
106
|
+
export function TestComponent() {
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
<TextArea label="First" label="Second" />
|
|
110
|
+
<div className="container" className="wrapper">Content</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
const result = applyTransform(source)
|
|
117
|
+
|
|
118
|
+
expect(result).not.toBeNull()
|
|
119
|
+
|
|
120
|
+
// TextArea duplicates should be removed
|
|
121
|
+
expect(result).toContain('label="First"')
|
|
122
|
+
expect(result).not.toContain('label="Second"')
|
|
123
|
+
|
|
124
|
+
// Other elements should remain unchanged
|
|
125
|
+
expect(result).toContain('className="container"')
|
|
126
|
+
expect(result).toContain('className="wrapper"')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { removeDuplicateKeys } from "../../shared/actions/removeDuplicateKeys"
|
|
2
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
3
|
+
|
|
4
|
+
export default attributeTransformFactory({
|
|
5
|
+
targetComponent: "TextArea",
|
|
6
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
7
|
+
transform: removeDuplicateKeys,
|
|
8
|
+
})
|