@planningcenter/tapestry-migration-cli 3.4.1-rc.9 → 3.5.0

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 (39) hide show
  1. package/README.md +22 -0
  2. package/dist/tapestry-react-shim.cjs +5 -1
  3. package/package.json +3 -3
  4. package/src/availableComponents.ts +13 -0
  5. package/src/components/dropdown/index.test.ts +244 -4
  6. package/src/components/dropdown/index.ts +28 -2
  7. package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
  8. package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
  9. package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
  10. package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
  11. package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
  12. package/src/components/dropdown/transforms/itemToAction.ts +14 -2
  13. package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
  14. package/src/components/dropdown/transforms/linkToLink.ts +13 -2
  15. package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
  16. package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
  17. package/src/components/dropdown/transforms/placementIdToMenu.test.ts +140 -0
  18. package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
  19. package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
  20. package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
  21. package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
  22. package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
  23. package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
  24. package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
  25. package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
  26. package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
  27. package/src/components/dropdown/transforms/variantToKind.test.ts +292 -0
  28. package/src/components/dropdown/transforms/variantToKind.ts +138 -0
  29. package/src/components/dropdown/transforms/wrapIconTrigger.test.ts +336 -0
  30. package/src/components/dropdown/transforms/wrapIconTrigger.ts +233 -0
  31. package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
  32. package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
  33. package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
  34. package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
  35. package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
  36. package/src/components/shared/conditions/isChildOf.test.ts +89 -0
  37. package/src/components/shared/conditions/isChildOf.ts +43 -0
  38. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
  39. package/src/index.ts +5 -18
@@ -0,0 +1,153 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./wrapMenu"
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("wrapMenu transform", () => {
18
+ describe("wrapping children", () => {
19
+ it("wraps Dropdown children in DropdownMenu", () => {
20
+ const input = `
21
+ import { Dropdown } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return (
25
+ <Dropdown onClose={handleClose}>
26
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
27
+ </Dropdown>
28
+ )
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain("<DropdownMenu>")
34
+ expect(result).toContain("</DropdownMenu>")
35
+ expect(result).toContain("Dropdown.Item")
36
+ })
37
+
38
+ it("preserves children inside DropdownMenu", () => {
39
+ const input = `
40
+ import { Dropdown } from "@planningcenter/tapestry-react"
41
+
42
+ export default function Test() {
43
+ return (
44
+ <Dropdown onClose={handleClose}>
45
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
46
+ <Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
47
+ </Dropdown>
48
+ )
49
+ }
50
+ `.trim()
51
+
52
+ const result = applyTransform(input)
53
+ const itemCount = (result?.match(/<Dropdown\.Item/g) || []).length
54
+ expect(itemCount).toBe(2)
55
+ })
56
+
57
+ it("adds DropdownMenu import from tapestry", () => {
58
+ const input = `
59
+ import { Dropdown } from "@planningcenter/tapestry-react"
60
+
61
+ export default function Test() {
62
+ return (
63
+ <Dropdown onClose={handleClose}>
64
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
65
+ </Dropdown>
66
+ )
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain('from "@planningcenter/tapestry"')
72
+ expect(result).toContain("DropdownMenu")
73
+ })
74
+
75
+ it("does not touch the tapestry-react import statement", () => {
76
+ const input = `
77
+ import { Dropdown } from "@planningcenter/tapestry-react"
78
+
79
+ export default function Test() {
80
+ return (
81
+ <Dropdown onClose={handleClose}>
82
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
83
+ </Dropdown>
84
+ )
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain(
90
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
91
+ )
92
+ })
93
+ })
94
+
95
+ describe("no changes scenarios", () => {
96
+ it("returns null when DropdownMenu already exists", () => {
97
+ const input = `
98
+ import { Dropdown } from "@planningcenter/tapestry-react"
99
+
100
+ export default function Test() {
101
+ return (
102
+ <Dropdown>
103
+ <DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
104
+ <DropdownMenu>
105
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
106
+ </DropdownMenu>
107
+ </Dropdown>
108
+ )
109
+ }
110
+ `.trim()
111
+
112
+ const result = applyTransform(input)
113
+ expect(result).toBe(null)
114
+ })
115
+
116
+ it("wraps complex children in DropdownMenu and adds a TODO comment", () => {
117
+ const input = `
118
+ import { Dropdown } from "@planningcenter/tapestry-react"
119
+
120
+ export default function Test({ children }) {
121
+ return <Dropdown onClose={handleClose}>{children}</Dropdown>
122
+ }
123
+ `.trim()
124
+
125
+ const result = applyTransform(input)
126
+ expect(result).toContain("<DropdownMenu>")
127
+ expect(result).toContain(
128
+ "TODO: tapestry-migration (children): complex children cannot be automatically converted to DropdownAction/DropdownLink"
129
+ )
130
+ })
131
+
132
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
133
+ const input = `
134
+ import { Dropdown } from "some-other-library"
135
+
136
+ export default function Test() {
137
+ return (
138
+ <Dropdown>
139
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
140
+ </Dropdown>
141
+ )
142
+ }
143
+ `.trim()
144
+
145
+ const result = applyTransform(input)
146
+ expect(result).toBe(null)
147
+ })
148
+
149
+ it("returns null for empty file", () => {
150
+ expect(applyTransform("")).toBe(null)
151
+ })
152
+ })
153
+ })
@@ -0,0 +1,54 @@
1
+ import { JSXElement, Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
5
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
6
+
7
+ const transform: Transform = attributeTransformFactory({
8
+ targetComponent: "Dropdown",
9
+ targetPackage: "@planningcenter/tapestry-react",
10
+ transform: (element, { j, source }) => {
11
+ const children = element.children ?? []
12
+
13
+ if (
14
+ children.some(
15
+ (child): child is JSXElement =>
16
+ child.type === "JSXElement" &&
17
+ child.openingElement.name.type === "JSXIdentifier" &&
18
+ child.openingElement.name.name === "DropdownMenu"
19
+ )
20
+ ) {
21
+ return false
22
+ }
23
+
24
+ if (!children.some((child) => child.type === "JSXElement")) {
25
+ addComment({
26
+ element,
27
+ j,
28
+ scope: "children",
29
+ source,
30
+ text: "complex children cannot be automatically converted to DropdownAction/DropdownLink — verify contents",
31
+ })
32
+ }
33
+
34
+ element.children = [
35
+ j.jsxElement(
36
+ j.jsxOpeningElement(j.jsxIdentifier("DropdownMenu"), [], false),
37
+ j.jsxClosingElement(j.jsxIdentifier("DropdownMenu")),
38
+ children
39
+ ),
40
+ ]
41
+
42
+ addImport({
43
+ component: "DropdownMenu",
44
+ fromPackage: "@planningcenter/tapestry-react",
45
+ j,
46
+ pkg: "@planningcenter/tapestry",
47
+ source,
48
+ })
49
+
50
+ return true
51
+ },
52
+ })
53
+
54
+ export default transform
@@ -0,0 +1,283 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./wrapTrigger"
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("wrapTrigger transform", () => {
18
+ describe("title prop", () => {
19
+ it("converts title string to DropdownTrigger + DropdownButton", () => {
20
+ const input = `
21
+ import { Dropdown } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return (
25
+ <Dropdown title="Actions">
26
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
27
+ </Dropdown>
28
+ )
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain("<DropdownTrigger>")
34
+ expect(result).toContain("</DropdownTrigger>")
35
+ expect(result).toContain('<DropdownButton label="Actions"')
36
+ expect(result).not.toContain('title="Actions"')
37
+ })
38
+
39
+ it("converts dynamic title expression to DropdownButton label", () => {
40
+ const input = `
41
+ import { Dropdown } from "@planningcenter/tapestry-react"
42
+
43
+ export default function Test() {
44
+ return (
45
+ <Dropdown title={menuTitle}>
46
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
47
+ </Dropdown>
48
+ )
49
+ }
50
+ `.trim()
51
+
52
+ const result = applyTransform(input)
53
+ expect(result).toContain("DropdownButton")
54
+ expect(result).toContain("label={menuTitle}")
55
+ expect(result).not.toContain("title=")
56
+ })
57
+
58
+ it("preserves existing Dropdown children after trigger insertion", () => {
59
+ const input = `
60
+ import { Dropdown } from "@planningcenter/tapestry-react"
61
+
62
+ export default function Test() {
63
+ return (
64
+ <Dropdown title="Actions">
65
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
66
+ <Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
67
+ </Dropdown>
68
+ )
69
+ }
70
+ `.trim()
71
+
72
+ const result = applyTransform(input)
73
+ expect(result).toContain("<DropdownTrigger>")
74
+ expect(result).toContain("Dropdown.Item")
75
+ })
76
+ })
77
+
78
+ describe("triggerElement prop", () => {
79
+ it("wraps triggerElement JSX value in DropdownTrigger", () => {
80
+ const input = `
81
+ import { Dropdown } from "@planningcenter/tapestry-react"
82
+
83
+ export default function Test() {
84
+ return (
85
+ <Dropdown triggerElement={<DropdownButton label="Actions" />}>
86
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
87
+ </Dropdown>
88
+ )
89
+ }
90
+ `.trim()
91
+
92
+ const result = applyTransform(input)
93
+ expect(result).toContain("<DropdownTrigger>")
94
+ expect(result).toContain("</DropdownTrigger>")
95
+ expect(result).toContain("DropdownButton")
96
+ expect(result).not.toContain("triggerElement=")
97
+ })
98
+
99
+ it("triggerElement takes precedence over title", () => {
100
+ const input = `
101
+ import { Dropdown } from "@planningcenter/tapestry-react"
102
+
103
+ export default function Test() {
104
+ return (
105
+ <Dropdown title="Ignored" triggerElement={<DropdownButton label="Used" />}>
106
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
107
+ </Dropdown>
108
+ )
109
+ }
110
+ `.trim()
111
+
112
+ const result = applyTransform(input)
113
+ expect(result).toContain('label="Used"')
114
+ expect(result).not.toContain("title=")
115
+ expect(result).not.toContain("triggerElement=")
116
+ })
117
+ })
118
+
119
+ describe("attribute preservation", () => {
120
+ it("preserves other Dropdown props", () => {
121
+ const input = `
122
+ import { Dropdown } from "@planningcenter/tapestry-react"
123
+
124
+ export default function Test() {
125
+ return (
126
+ <Dropdown title="Actions" onClose={handleClose} placement="bottom-end">
127
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
128
+ </Dropdown>
129
+ )
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain("onClose={handleClose}")
135
+ expect(result).toContain('placement="bottom-end"')
136
+ })
137
+
138
+ it("does not touch the tapestry-react import statement", () => {
139
+ const input = `
140
+ import { Dropdown } from "@planningcenter/tapestry-react"
141
+
142
+ export default function Test() {
143
+ return (
144
+ <Dropdown title="Actions">
145
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
146
+ </Dropdown>
147
+ )
148
+ }
149
+ `.trim()
150
+
151
+ const result = applyTransform(input)
152
+ expect(result).toContain(
153
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
154
+ )
155
+ })
156
+
157
+ it("adds DropdownTrigger and DropdownButton imports from tapestry when title is used", () => {
158
+ const input = `
159
+ import { Dropdown } from "@planningcenter/tapestry-react"
160
+
161
+ export default function Test() {
162
+ return (
163
+ <Dropdown title="Actions">
164
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
165
+ </Dropdown>
166
+ )
167
+ }
168
+ `.trim()
169
+
170
+ const result = applyTransform(input)
171
+ expect(result).toContain('from "@planningcenter/tapestry"')
172
+ expect(result).toContain("DropdownTrigger")
173
+ expect(result).toContain("DropdownButton")
174
+ })
175
+
176
+ it("adds only DropdownTrigger import when triggerElement is provided", () => {
177
+ const input = `
178
+ import { Dropdown } from "@planningcenter/tapestry-react"
179
+
180
+ export default function Test() {
181
+ return (
182
+ <Dropdown triggerElement={<MyButton />}>
183
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
184
+ </Dropdown>
185
+ )
186
+ }
187
+ `.trim()
188
+
189
+ const result = applyTransform(input)
190
+ const tapestryImport =
191
+ result?.match(/import {[^}]+} from "@planningcenter\/tapestry"/)?.[0] ??
192
+ ""
193
+ expect(tapestryImport).toContain("DropdownTrigger")
194
+ expect(tapestryImport).not.toContain("DropdownButton")
195
+ })
196
+ })
197
+
198
+ describe("multiple dropdowns", () => {
199
+ it("transforms all Dropdown elements in the file", () => {
200
+ const input = `
201
+ import { Dropdown } from "@planningcenter/tapestry-react"
202
+
203
+ export default function Test() {
204
+ return (
205
+ <div>
206
+ <Dropdown title="First">
207
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
208
+ </Dropdown>
209
+ <Dropdown title="Second">
210
+ <Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
211
+ </Dropdown>
212
+ </div>
213
+ )
214
+ }
215
+ `.trim()
216
+
217
+ const result = applyTransform(input)
218
+ const count = (result?.match(/<DropdownTrigger>/g) || []).length
219
+ expect(count).toBe(2)
220
+ })
221
+ })
222
+
223
+ describe("no changes scenarios", () => {
224
+ it("returns null when Dropdown has no title or triggerElement", () => {
225
+ const input = `
226
+ import { Dropdown } from "@planningcenter/tapestry-react"
227
+
228
+ export default function Test() {
229
+ return (
230
+ <Dropdown onClose={handleClose}>
231
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
232
+ </Dropdown>
233
+ )
234
+ }
235
+ `.trim()
236
+
237
+ const result = applyTransform(input)
238
+ expect(result).toBe(null)
239
+ })
240
+
241
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
242
+ const input = `
243
+ import { Dropdown } from "some-other-library"
244
+
245
+ export default function Test() {
246
+ return (
247
+ <Dropdown title="Actions">
248
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
249
+ </Dropdown>
250
+ )
251
+ }
252
+ `.trim()
253
+
254
+ const result = applyTransform(input)
255
+ expect(result).toBe(null)
256
+ })
257
+
258
+ it("returns null for empty file", () => {
259
+ expect(applyTransform("")).toBe(null)
260
+ })
261
+
262
+ it("wraps non-JSX triggerElement expression as a child with a TODO comment", () => {
263
+ const input = `
264
+ import { Dropdown } from "@planningcenter/tapestry-react"
265
+
266
+ export default function Test() {
267
+ return (
268
+ <Dropdown triggerElement={myTrigger}>
269
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
270
+ </Dropdown>
271
+ )
272
+ }
273
+ `.trim()
274
+
275
+ const result = applyTransform(input)
276
+ expect(result).toContain("<DropdownTrigger>")
277
+ expect(result).toContain("{myTrigger}")
278
+ expect(result).not.toContain("triggerElement=")
279
+ expect(result).toContain("TODO")
280
+ expect(result).toContain("triggerElement")
281
+ })
282
+ })
283
+ })
@@ -0,0 +1,98 @@
1
+ import {
2
+ JSXAttribute,
3
+ JSXElement,
4
+ JSXExpressionContainer,
5
+ Transform,
6
+ } from "jscodeshift"
7
+
8
+ import { addComment } from "../../shared/actions/addComment"
9
+ import { getAttribute } from "../../shared/actions/getAttribute"
10
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
11
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
12
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
13
+
14
+ const transform: Transform = attributeTransformFactory({
15
+ targetComponent: "Dropdown",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j, source }) => {
18
+ const titleAttr = getAttribute({ element, name: "title" })
19
+ const triggerElementAttr = getAttribute({ element, name: "triggerElement" })
20
+
21
+ if (!titleAttr && !triggerElementAttr) return false
22
+
23
+ let triggerChild: JSXElement | JSXExpressionContainer | null = null
24
+
25
+ if (triggerElementAttr) {
26
+ const attrValue = triggerElementAttr.value
27
+ if (
28
+ attrValue?.type === "JSXExpressionContainer" &&
29
+ attrValue.expression.type !== "JSXEmptyExpression"
30
+ ) {
31
+ if (attrValue.expression.type === "JSXElement") {
32
+ triggerChild = attrValue.expression
33
+ } else {
34
+ triggerChild = j.jsxExpressionContainer(attrValue.expression)
35
+ addComment({
36
+ element,
37
+ j,
38
+ scope: "triggerElement",
39
+ source,
40
+ text: "triggerElement was not a JSX element — verify this renders correctly inside DropdownTrigger",
41
+ })
42
+ }
43
+ }
44
+ }
45
+
46
+ if (!triggerChild && titleAttr) {
47
+ const labelValue = titleAttr.value
48
+ if (!labelValue) return false
49
+
50
+ const labelAttr: JSXAttribute = j.jsxAttribute(
51
+ j.jsxIdentifier("label"),
52
+ labelValue
53
+ )
54
+
55
+ triggerChild = j.jsxElement(
56
+ j.jsxOpeningElement(
57
+ j.jsxIdentifier("DropdownButton"),
58
+ [labelAttr],
59
+ true
60
+ ),
61
+ null,
62
+ []
63
+ )
64
+ addImport({
65
+ component: "DropdownButton",
66
+ fromPackage: "@planningcenter/tapestry-react",
67
+ j,
68
+ pkg: "@planningcenter/tapestry",
69
+ source,
70
+ })
71
+ }
72
+
73
+ if (!triggerChild) return false
74
+
75
+ const triggerWrapper = j.jsxElement(
76
+ j.jsxOpeningElement(j.jsxIdentifier("DropdownTrigger"), [], false),
77
+ j.jsxClosingElement(j.jsxIdentifier("DropdownTrigger")),
78
+ [triggerChild]
79
+ )
80
+
81
+ removeAttribute("title", { element, j, source })
82
+ removeAttribute("triggerElement", { element, j, source })
83
+
84
+ element.children = [triggerWrapper, ...(element.children ?? [])]
85
+
86
+ addImport({
87
+ component: "DropdownTrigger",
88
+ fromPackage: "@planningcenter/tapestry-react",
89
+ j,
90
+ pkg: "@planningcenter/tapestry",
91
+ source,
92
+ })
93
+
94
+ return true
95
+ },
96
+ })
97
+
98
+ export default transform
@@ -156,19 +156,6 @@ function Test() {
156
156
  expect(result).toContain("TODO: tapestry-migration (hover)")
157
157
  })
158
158
 
159
- it("should flag autoFocus", () => {
160
- const input = `
161
- import { Input } from "@planningcenter/tapestry-react"
162
-
163
- function Test() {
164
- return <Input autoFocus label="Name" />
165
- }
166
- `.trim()
167
-
168
- const result = applyTransform(input)
169
- expect(result).toContain("TODO: tapestry-migration (autoFocus)")
170
- })
171
-
172
159
  it("should flag readOnlyBackgroundColor", () => {
173
160
  const input = `
174
161
  import { Input } from "@planningcenter/tapestry-react"
@@ -231,6 +218,20 @@ function Test() {
231
218
  expect(result).not.toContain("TODO: tapestry-migration")
232
219
  })
233
220
 
221
+ it("should not flag autoFocus", () => {
222
+ const input = `
223
+ import { Input } from "@planningcenter/tapestry-react"
224
+
225
+ function Test() {
226
+ return <Input autoFocus label="Name" />
227
+ }
228
+ `.trim()
229
+
230
+ const result = applyTransform(input)
231
+ expect(result).not.toContain("TODO: tapestry-migration")
232
+ expect(result).toContain("autoFocus")
233
+ })
234
+
234
235
  it("should not flag standard HTML attributes", () => {
235
236
  const input = `
236
237
  import { Input } from "@planningcenter/tapestry-react"