@planningcenter/tapestry-migration-cli 3.1.0-rc.6 → 3.1.0-rc.7

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.
Files changed (52) hide show
  1. package/package.json +3 -3
  2. package/src/components/button/transforms/convertStyleProps.test.ts +97 -0
  3. package/src/components/button/transforms/removeTypeButton.test.ts +0 -1
  4. package/src/components/checkbox/transforms/moveCheckboxImport.test.ts +3 -0
  5. package/src/components/input/index.ts +66 -0
  6. package/src/components/input/transformableInput.ts +8 -0
  7. package/src/components/input/transforms/auditSpreadProps.test.ts +192 -0
  8. package/src/components/input/transforms/auditSpreadProps.ts +26 -0
  9. package/src/components/input/transforms/autoWidthTransform.test.ts +172 -0
  10. package/src/components/input/transforms/autoWidthTransform.ts +41 -0
  11. package/src/components/input/transforms/convertStyleProps.test.ts +128 -0
  12. package/src/components/input/transforms/convertStyleProps.ts +12 -0
  13. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.test.ts +186 -0
  14. package/src/components/input/transforms/highlightOnInteractionToSelectTextOnFocus.ts +27 -0
  15. package/src/components/input/transforms/inputLabelToLabelProp.test.ts +319 -0
  16. package/src/components/input/transforms/inputLabelToLabelProp.ts +203 -0
  17. package/src/components/input/transforms/mergeFieldIntoInput.test.ts +391 -0
  18. package/src/components/input/transforms/mergeFieldIntoInput.ts +213 -0
  19. package/src/components/input/transforms/mergeInputLabel.test.ts +458 -0
  20. package/src/components/input/transforms/mergeInputLabel.ts +204 -0
  21. package/src/components/input/transforms/moveInputImport.test.ts +166 -0
  22. package/src/components/input/transforms/moveInputImport.ts +14 -0
  23. package/src/components/input/transforms/numberFieldAddTypeNumber.test.ts +92 -0
  24. package/src/components/input/transforms/numberFieldAddTypeNumber.ts +14 -0
  25. package/src/components/input/transforms/numberFieldRenameToInput.test.ts +126 -0
  26. package/src/components/input/transforms/numberFieldRenameToInput.ts +9 -0
  27. package/src/components/input/transforms/removeAsInput.test.ts +139 -0
  28. package/src/components/input/transforms/removeAsInput.ts +20 -0
  29. package/src/components/input/transforms/removeDuplicateKeys.test.ts +302 -0
  30. package/src/components/input/transforms/removeDuplicateKeys.ts +10 -0
  31. package/src/components/input/transforms/removeInputBox.test.ts +352 -0
  32. package/src/components/input/transforms/removeInputBox.ts +109 -0
  33. package/src/components/input/transforms/removeRedundantAriaLabel.test.ts +128 -0
  34. package/src/components/input/transforms/removeRedundantAriaLabel.ts +21 -0
  35. package/src/components/input/transforms/removeTypeText.test.ts +160 -0
  36. package/src/components/input/transforms/removeTypeText.ts +18 -0
  37. package/src/components/input/transforms/sizeMapping.test.ts +198 -0
  38. package/src/components/input/transforms/sizeMapping.ts +17 -0
  39. package/src/components/input/transforms/skipRenderSideProps.test.ts +236 -0
  40. package/src/components/input/transforms/skipRenderSideProps.ts +27 -0
  41. package/src/components/input/transforms/stateToInvalid.test.ts +208 -0
  42. package/src/components/input/transforms/stateToInvalid.ts +59 -0
  43. package/src/components/input/transforms/stateToInvalidTernary.test.ts +159 -0
  44. package/src/components/input/transforms/stateToInvalidTernary.ts +13 -0
  45. package/src/components/input/transforms/unsupportedProps.test.ts +566 -0
  46. package/src/components/input/transforms/unsupportedProps.ts +84 -0
  47. package/src/components/link/transforms/reviewStyles.test.ts +0 -1
  48. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +52 -0
  49. package/src/components/shared/transformFactories/helpers/manageImports.ts +14 -12
  50. package/src/components/shared/transformFactories/sizeMappingFactory.ts +9 -2
  51. package/src/components/shared/transformFactories/stylePropTransformFactory.ts +54 -16
  52. package/src/components/shared/transformFactories/ternaryConditionalToPropFactory.ts +65 -0
@@ -0,0 +1,128 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./convertStyleProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string, options = {}) {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ options
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("convertStyleProps transform", () => {
19
+ describe("visible prop", () => {
20
+ it("should convert visible={false} to display: none", () => {
21
+ const source = `
22
+ import { Input } from "@planningcenter/tapestry-react"
23
+
24
+ export function TestComponent() {
25
+ return <Input visible={false} label="Name" />
26
+ }
27
+ `
28
+
29
+ const result = applyTransform(source)
30
+ expect(result).toContain("style={{")
31
+ expect(result).toContain('display: "none"')
32
+ expect(result).not.toContain("visible={false}")
33
+ })
34
+
35
+ it("should remove visible={true} with no style changes", () => {
36
+ const source = `
37
+ import { Input } from "@planningcenter/tapestry-react"
38
+
39
+ export function TestComponent() {
40
+ return <Input visible={true} label="Name" />
41
+ }
42
+ `
43
+
44
+ const result = applyTransform(source)
45
+ expect(result).not.toContain("visible={true}")
46
+ expect(result).not.toContain("style={{")
47
+ })
48
+ })
49
+
50
+ describe("style prop removal", () => {
51
+ it("should remove marginTop prop", () => {
52
+ const source = `
53
+ import { Input } from "@planningcenter/tapestry-react"
54
+
55
+ export function TestComponent() {
56
+ return <Input marginTop={16} label="Name" />
57
+ }
58
+ `
59
+
60
+ const result = applyTransform(source)
61
+ expect(result).not.toContain("marginTop=")
62
+ })
63
+
64
+ it("should remove alignItems prop", () => {
65
+ const source = `
66
+ import { Input } from "@planningcenter/tapestry-react"
67
+
68
+ export function TestComponent() {
69
+ return <Input alignItems="center" label="Name" />
70
+ }
71
+ `
72
+
73
+ const result = applyTransform(source)
74
+ expect(result).not.toContain("alignItems=")
75
+ })
76
+ })
77
+
78
+ describe("import handling", () => {
79
+ it("should not affect imports", () => {
80
+ const source = `
81
+ import { Input } from "@planningcenter/tapestry-react"
82
+
83
+ export function TestComponent() {
84
+ return <Input visible={false} label="Name" />
85
+ }
86
+ `
87
+
88
+ const result = applyTransform(source)
89
+ expect(result).toContain(
90
+ 'import { Input } from "@planningcenter/tapestry-react"'
91
+ )
92
+ })
93
+ })
94
+
95
+ describe("edge cases", () => {
96
+ it("should not affect Input with no style props", () => {
97
+ const source = `
98
+ import { Input } from "@planningcenter/tapestry-react"
99
+
100
+ export function TestComponent() {
101
+ return <Input label="Name" placeholder="Enter name" />
102
+ }
103
+ `
104
+
105
+ const result = applyTransform(source)
106
+ expect(result).toBe(source)
107
+ })
108
+
109
+ it("should not affect other components", () => {
110
+ const source = `
111
+ import { Input, Button } from "@planningcenter/tapestry-react"
112
+
113
+ export function TestComponent() {
114
+ return (
115
+ <div>
116
+ <Button visible={false}>Hidden</Button>
117
+ <Input visible={false} label="Name" />
118
+ </div>
119
+ )
120
+ }
121
+ `
122
+
123
+ const result = applyTransform(source)
124
+ expect(result).toContain("visible={false}")
125
+ expect(result).toContain('display: "none"')
126
+ })
127
+ })
128
+ })
@@ -0,0 +1,12 @@
1
+ import { stackViewPlugin } from "../../../stubs/stackViewPlugin"
2
+ import { stylePropTransformFactory } from "../../shared/transformFactories/stylePropTransformFactory"
3
+ import { transformableInput } from "../transformableInput"
4
+
5
+ export default stylePropTransformFactory({
6
+ condition: transformableInput,
7
+ plugin: stackViewPlugin,
8
+ stylesToKeep: ["visible"],
9
+ stylesToRemove: [],
10
+ targetComponent: "Input",
11
+ targetPackage: "@planningcenter/tapestry-react",
12
+ })
@@ -0,0 +1,186 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./highlightOnInteractionToSelectTextOnFocus"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string, verbose = false): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ const result = transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ { verbose }
14
+ ) as string | null
15
+ return result || source
16
+ }
17
+
18
+ describe("highlightOnInteractionToSelectTextOnFocus transform", () => {
19
+ describe("basic rename", () => {
20
+ it("should rename highlightOnInteraction to selectTextOnFocus", () => {
21
+ const input = `
22
+ import { Input } from "@planningcenter/tapestry-react"
23
+
24
+ function Test() {
25
+ return <Input highlightOnInteraction label="Name" />
26
+ }
27
+ `.trim()
28
+
29
+ const result = applyTransform(input)
30
+ expect(result).toContain("selectTextOnFocus")
31
+ expect(result).not.toContain("highlightOnInteraction")
32
+ })
33
+
34
+ it("should rename highlightOnInteraction={true}", () => {
35
+ const input = `
36
+ import { Input } from "@planningcenter/tapestry-react"
37
+
38
+ function Test() {
39
+ return <Input highlightOnInteraction={true} label="Name" />
40
+ }
41
+ `.trim()
42
+
43
+ const result = applyTransform(input)
44
+ expect(result).toContain("selectTextOnFocus={true}")
45
+ expect(result).not.toContain("highlightOnInteraction")
46
+ })
47
+
48
+ it("should rename highlightOnInteraction={false}", () => {
49
+ const input = `
50
+ import { Input } from "@planningcenter/tapestry-react"
51
+
52
+ function Test() {
53
+ return <Input highlightOnInteraction={false} label="Name" />
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).toContain("selectTextOnFocus={false}")
59
+ expect(result).not.toContain("highlightOnInteraction")
60
+ })
61
+
62
+ it("should rename highlightOnInteraction with variable expression", () => {
63
+ const input = `
64
+ import { Input } from "@planningcenter/tapestry-react"
65
+
66
+ function Test() {
67
+ return <Input highlightOnInteraction={shouldHighlight} label="Name" />
68
+ }
69
+ `.trim()
70
+
71
+ const result = applyTransform(input)
72
+ expect(result).toContain("selectTextOnFocus={shouldHighlight}")
73
+ expect(result).not.toContain("highlightOnInteraction")
74
+ })
75
+
76
+ it("should preserve other props", () => {
77
+ const input = `
78
+ import { Input } from "@planningcenter/tapestry-react"
79
+
80
+ function Test() {
81
+ return (
82
+ <Input
83
+ highlightOnInteraction
84
+ label="Name"
85
+ placeholder="Enter name"
86
+ disabled
87
+ onChange={handleChange}
88
+ />
89
+ )
90
+ }
91
+ `.trim()
92
+
93
+ const result = applyTransform(input)
94
+ expect(result).toContain("selectTextOnFocus")
95
+ expect(result).not.toContain("highlightOnInteraction")
96
+ expect(result).toContain('label="Name"')
97
+ expect(result).toContain('placeholder="Enter name"')
98
+ expect(result).toContain("disabled")
99
+ expect(result).toContain("onChange={handleChange}")
100
+ })
101
+ })
102
+
103
+ describe("edge cases", () => {
104
+ it("should not affect Input without highlightOnInteraction prop", () => {
105
+ const input = `
106
+ import { Input } from "@planningcenter/tapestry-react"
107
+
108
+ function Test() {
109
+ return <Input label="Name" />
110
+ }
111
+ `.trim()
112
+
113
+ const result = applyTransform(input)
114
+ expect(result).toBe(input)
115
+ })
116
+
117
+ it("should not affect other components", () => {
118
+ const input = `
119
+ import { Input, Button } from "@planningcenter/tapestry-react"
120
+
121
+ function Test() {
122
+ return (
123
+ <div>
124
+ <Button highlightOnInteraction>Click</Button>
125
+ <Input highlightOnInteraction label="Name" />
126
+ </div>
127
+ )
128
+ }
129
+ `.trim()
130
+
131
+ const result = applyTransform(input)
132
+ expect(result).toContain("<Button highlightOnInteraction>Click</Button>")
133
+ expect(result).toContain("selectTextOnFocus")
134
+ })
135
+
136
+ it("should handle multiple Input components", () => {
137
+ const input = `
138
+ import { Input } from "@planningcenter/tapestry-react"
139
+
140
+ function Test() {
141
+ return (
142
+ <div>
143
+ <Input highlightOnInteraction label="First" />
144
+ <Input highlightOnInteraction={true} label="Second" />
145
+ </div>
146
+ )
147
+ }
148
+ `.trim()
149
+
150
+ const result = applyTransform(input)
151
+ expect(result).not.toContain("highlightOnInteraction")
152
+ const matches = result.match(/selectTextOnFocus/g)
153
+ expect(matches).toHaveLength(2)
154
+ })
155
+
156
+ it("should add CHANGED comment when verbose is enabled", () => {
157
+ const input = `
158
+ import { Input } from "@planningcenter/tapestry-react"
159
+
160
+ function Test() {
161
+ return <Input highlightOnInteraction label="Name" />
162
+ }
163
+ `.trim()
164
+
165
+ const result = applyTransform(input, true)
166
+ expect(result).toContain("selectTextOnFocus")
167
+ expect(result).not.toContain("highlightOnInteraction=")
168
+ expect(result).toContain(
169
+ "CHANGED: tapestry-migration (selectTextOnFocus): renamed from highlightOnInteraction"
170
+ )
171
+ })
172
+
173
+ it("should not transform if not imported from @planningcenter/tapestry-react", () => {
174
+ const input = `
175
+ import { Input } from "other-library"
176
+
177
+ function Test() {
178
+ return <Input highlightOnInteraction label="Name" />
179
+ }
180
+ `.trim()
181
+
182
+ const result = applyTransform(input)
183
+ expect(result).toBe(input)
184
+ })
185
+ })
186
+ })
@@ -0,0 +1,27 @@
1
+ import { transformAttributeName } from "../../shared/actions/transformAttributeName"
2
+ import { andConditions } from "../../shared/conditions/andConditions"
3
+ import { hasAttribute } from "../../shared/conditions/hasAttribute"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+ import { transformableInput } from "../transformableInput"
6
+
7
+ const transform = attributeTransformFactory({
8
+ condition: andConditions(
9
+ hasAttribute("highlightOnInteraction"),
10
+ transformableInput
11
+ ),
12
+ targetComponent: "Input",
13
+ targetPackage: "@planningcenter/tapestry-react",
14
+ transform: (element, { j, options }) => {
15
+ return transformAttributeName(
16
+ "highlightOnInteraction",
17
+ "selectTextOnFocus",
18
+ {
19
+ element,
20
+ j,
21
+ options,
22
+ }
23
+ )
24
+ },
25
+ })
26
+
27
+ export default transform
@@ -0,0 +1,319 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./inputLabelToLabelProp"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
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("inputLabelToLabelProp transform", () => {
18
+ describe("successful conversions", () => {
19
+ it("InputLabel before Input → label prop added, InputLabel removed", () => {
20
+ const input = `
21
+ import { Input, StackView } from "@planningcenter/tapestry-react"
22
+
23
+ function Test() {
24
+ return (
25
+ <StackView>
26
+ <Input.InputLabel htmlFor="f1">Name</Input.InputLabel>
27
+ <Input id="f1" />
28
+ </StackView>
29
+ )
30
+ }
31
+ `.trim()
32
+
33
+ const result = applyTransform(input)
34
+ expect(result).not.toBeNull()
35
+ expect(result).toContain('label="Name"')
36
+ expect(result).not.toContain("<Input.InputLabel")
37
+ })
38
+
39
+ it("Input before InputLabel → label prop added, InputLabel removed", () => {
40
+ const input = `
41
+ import { Input, StackView } from "@planningcenter/tapestry-react"
42
+
43
+ function Test() {
44
+ return (
45
+ <StackView>
46
+ <Input id="f1" />
47
+ <Input.InputLabel htmlFor="f1">Email</Input.InputLabel>
48
+ </StackView>
49
+ )
50
+ }
51
+ `.trim()
52
+
53
+ const result = applyTransform(input)
54
+ expect(result).not.toBeNull()
55
+ expect(result).toContain('label="Email"')
56
+ expect(result).not.toContain("<Input.InputLabel")
57
+ })
58
+
59
+ it("no axis prop (default vertical) → transforms", () => {
60
+ const input = `
61
+ import { Input, StackView } from "@planningcenter/tapestry-react"
62
+
63
+ function Test() {
64
+ return (
65
+ <StackView>
66
+ <Input.InputLabel>Address</Input.InputLabel>
67
+ <Input id="addr" />
68
+ </StackView>
69
+ )
70
+ }
71
+ `.trim()
72
+
73
+ const result = applyTransform(input)
74
+ expect(result).not.toBeNull()
75
+ expect(result).toContain('label="Address"')
76
+ })
77
+
78
+ it('axis="vertical" explicitly → transforms', () => {
79
+ const input = `
80
+ import { Input, StackView } from "@planningcenter/tapestry-react"
81
+
82
+ function Test() {
83
+ return (
84
+ <StackView axis="vertical">
85
+ <Input.InputLabel>City</Input.InputLabel>
86
+ <Input id="city" />
87
+ </StackView>
88
+ )
89
+ }
90
+ `.trim()
91
+
92
+ const result = applyTransform(input)
93
+ expect(result).not.toBeNull()
94
+ expect(result).toContain('label="City"')
95
+ })
96
+
97
+ it("axis={someVar} (dynamic, non-string-literal) → transforms (best-effort)", () => {
98
+ const input = `
99
+ import { Input, StackView } from "@planningcenter/tapestry-react"
100
+
101
+ function Test() {
102
+ return (
103
+ <StackView axis={someVar}>
104
+ <Input.InputLabel>State</Input.InputLabel>
105
+ <Input id="state" />
106
+ </StackView>
107
+ )
108
+ }
109
+ `.trim()
110
+
111
+ const result = applyTransform(input)
112
+ expect(result).not.toBeNull()
113
+ expect(result).toContain('label="State"')
114
+ })
115
+
116
+ it("multiple label+input pairs in one StackView → both transformed", () => {
117
+ const input = `
118
+ import { Input, StackView } from "@planningcenter/tapestry-react"
119
+
120
+ function Test() {
121
+ return (
122
+ <StackView>
123
+ <Input.InputLabel>First Name</Input.InputLabel>
124
+ <Input id="first" />
125
+ <Input.InputLabel>Last Name</Input.InputLabel>
126
+ <Input id="last" />
127
+ </StackView>
128
+ )
129
+ }
130
+ `.trim()
131
+
132
+ const result = applyTransform(input)
133
+ expect(result).not.toBeNull()
134
+ expect(result).toContain('label="First Name"')
135
+ expect(result).toContain('label="Last Name"')
136
+ expect(result).not.toContain("<Input.InputLabel")
137
+ })
138
+ })
139
+
140
+ describe("no-op / comment cases", () => {
141
+ it('axis="horizontal" → no transform, returns null', () => {
142
+ const input = `
143
+ import { Input, StackView } from "@planningcenter/tapestry-react"
144
+
145
+ function Test() {
146
+ return (
147
+ <StackView axis="horizontal">
148
+ <Input.InputLabel>Name</Input.InputLabel>
149
+ <Input id="f1" />
150
+ </StackView>
151
+ )
152
+ }
153
+ `.trim()
154
+
155
+ const result = applyTransform(input)
156
+ expect(result).toBeNull()
157
+ })
158
+
159
+ it("Input has renderLeft → no transform for that pair, returns null", () => {
160
+ const input = `
161
+ import { Input, StackView } from "@planningcenter/tapestry-react"
162
+
163
+ function Test() {
164
+ return (
165
+ <StackView>
166
+ <Input.InputLabel>Name</Input.InputLabel>
167
+ <Input id="f1" renderLeft={() => <span />} />
168
+ </StackView>
169
+ )
170
+ }
171
+ `.trim()
172
+
173
+ const result = applyTransform(input)
174
+ expect(result).toBeNull()
175
+ })
176
+
177
+ it("Input has renderRight → no transform for that pair, returns null", () => {
178
+ const input = `
179
+ import { Input, StackView } from "@planningcenter/tapestry-react"
180
+
181
+ function Test() {
182
+ return (
183
+ <StackView>
184
+ <Input.InputLabel>Name</Input.InputLabel>
185
+ <Input id="f1" renderRight={() => <span />} />
186
+ </StackView>
187
+ )
188
+ }
189
+ `.trim()
190
+
191
+ const result = applyTransform(input)
192
+ expect(result).toBeNull()
193
+ })
194
+
195
+ it("Input already has label prop → TODO comment added, InputLabel not removed", () => {
196
+ const input = `
197
+ import { Input, StackView } from "@planningcenter/tapestry-react"
198
+
199
+ function Test() {
200
+ return (
201
+ <StackView>
202
+ <Input.InputLabel>New Label</Input.InputLabel>
203
+ <Input id="f1" label="Existing" />
204
+ </StackView>
205
+ )
206
+ }
207
+ `.trim()
208
+
209
+ const result = applyTransform(input)
210
+ expect(result).not.toBeNull()
211
+ expect(result).toContain(
212
+ "TODO: tapestry-migration (inputLabelToLabelProp)"
213
+ )
214
+ expect(result).toContain("already has a label prop")
215
+ expect(result).toContain("New Label")
216
+ expect(result).toContain('label="Existing"')
217
+ // Should not add a second label attribute
218
+ expect(result?.match(/label=/g)?.length).toBe(1)
219
+ })
220
+
221
+ it("InputLabel has complex children (JSX nodes) → TODO comment added, InputLabel not removed", () => {
222
+ const input = `
223
+ import { Input, StackView } from "@planningcenter/tapestry-react"
224
+
225
+ function Test() {
226
+ return (
227
+ <StackView>
228
+ <Input.InputLabel><span>Name <em>*</em></span></Input.InputLabel>
229
+ <Input id="f1" />
230
+ </StackView>
231
+ )
232
+ }
233
+ `.trim()
234
+
235
+ const result = applyTransform(input)
236
+ expect(result).not.toBeNull()
237
+ expect(result).toContain(
238
+ "TODO: tapestry-migration (inputLabelToLabelProp)"
239
+ )
240
+ expect(result).toContain("complex and cannot be auto-converted")
241
+ expect(result).toContain("<Input.InputLabel")
242
+ expect(result).not.toContain('label="')
243
+ })
244
+
245
+ it("Input and InputLabel not adjacent (other element between them) → no transform", () => {
246
+ const input = `
247
+ import { Input, StackView } from "@planningcenter/tapestry-react"
248
+
249
+ function Test() {
250
+ return (
251
+ <StackView>
252
+ <Input.InputLabel>Name</Input.InputLabel>
253
+ <span>hint</span>
254
+ <Input id="f1" />
255
+ </StackView>
256
+ )
257
+ }
258
+ `.trim()
259
+
260
+ const result = applyTransform(input)
261
+ expect(result).toBeNull()
262
+ })
263
+
264
+ it("no StackView wrapper → no transform, returns null", () => {
265
+ const input = `
266
+ import { Input, StackView } from "@planningcenter/tapestry-react"
267
+
268
+ function Test() {
269
+ return (
270
+ <div>
271
+ <Input.InputLabel>Name</Input.InputLabel>
272
+ <Input id="f1" />
273
+ </div>
274
+ )
275
+ }
276
+ `.trim()
277
+
278
+ const result = applyTransform(input)
279
+ expect(result).toBeNull()
280
+ })
281
+
282
+ it("StackView from different package → no transform, returns null", () => {
283
+ const input = `
284
+ import { Input } from "@planningcenter/tapestry-react"
285
+ import { StackView } from "other-library"
286
+
287
+ function Test() {
288
+ return (
289
+ <StackView>
290
+ <Input.InputLabel>Name</Input.InputLabel>
291
+ <Input id="f1" />
292
+ </StackView>
293
+ )
294
+ }
295
+ `.trim()
296
+
297
+ const result = applyTransform(input)
298
+ expect(result).toBeNull()
299
+ })
300
+
301
+ it("StackView not imported at all → no transform, returns null", () => {
302
+ const input = `
303
+ import { Input } from "@planningcenter/tapestry-react"
304
+
305
+ function Test() {
306
+ return (
307
+ <StackView>
308
+ <Input.InputLabel>Name</Input.InputLabel>
309
+ <Input id="f1" />
310
+ </StackView>
311
+ )
312
+ }
313
+ `.trim()
314
+
315
+ const result = applyTransform(input)
316
+ expect(result).toBeNull()
317
+ })
318
+ })
319
+ })