@planningcenter/tapestry-migration-cli 3.4.1-rc.8 → 3.4.1

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 (32) hide show
  1. package/dist/tapestry-react-shim.cjs +34 -64
  2. package/package.json +3 -3
  3. package/src/components/dropdown/index.test.ts +177 -4
  4. package/src/components/dropdown/index.ts +24 -2
  5. package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
  6. package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
  7. package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
  8. package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
  9. package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
  10. package/src/components/dropdown/transforms/itemToAction.ts +14 -2
  11. package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
  12. package/src/components/dropdown/transforms/linkToLink.ts +13 -2
  13. package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
  14. package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
  15. package/src/components/dropdown/transforms/placementIdToMenu.test.ts +135 -0
  16. package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
  17. package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
  18. package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
  19. package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
  20. package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
  21. package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
  22. package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
  23. package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
  24. package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
  25. package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
  26. package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
  27. package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
  28. package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
  29. package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
  30. package/src/components/shared/conditions/isChildOf.test.ts +89 -0
  31. package/src/components/shared/conditions/isChildOf.ts +43 -0
  32. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
@@ -0,0 +1,67 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { transformElementName } from "../../shared/actions/transformElementName"
4
+ import { isChildOf } from "../../shared/conditions/isChildOf"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import {
7
+ addImport,
8
+ getImportName,
9
+ removeImportFromDeclaration,
10
+ } from "../../shared/transformFactories/helpers/manageImports"
11
+
12
+ const transform: Transform = (fileInfo, api, options) => {
13
+ const j = api.jscodeshift
14
+ const source = j(fileInfo.source)
15
+
16
+ const dropdownLocalName = getImportName(
17
+ "Dropdown",
18
+ "@planningcenter/tapestry-react",
19
+ { j, source }
20
+ )
21
+ if (!dropdownLocalName) return null
22
+
23
+ const result = attributeTransformFactory({
24
+ condition: isChildOf(dropdownLocalName, source, j),
25
+ targetComponent: "Divider",
26
+ targetPackage: "@planningcenter/tapestry-react",
27
+ transform: (element) =>
28
+ transformElementName({ element, name: "DropdownSeparator" }),
29
+ })(fileInfo, api, options)
30
+
31
+ if (!result) return null
32
+
33
+ const updatedSource = j(result as string)
34
+
35
+ addImport({
36
+ component: "DropdownSeparator",
37
+ fromPackage: "@planningcenter/tapestry-react",
38
+ j,
39
+ pkg: "@planningcenter/tapestry",
40
+ source: updatedSource,
41
+ })
42
+
43
+ const dividerLocalName = getImportName(
44
+ "Divider",
45
+ "@planningcenter/tapestry-react",
46
+ { j, source: updatedSource }
47
+ )
48
+ if (dividerLocalName) {
49
+ const dividerStillUsed =
50
+ updatedSource.find(j.JSXOpeningElement, {
51
+ name: { name: dividerLocalName },
52
+ }).length > 0
53
+
54
+ if (!dividerStillUsed) {
55
+ removeImportFromDeclaration(
56
+ updatedSource.find(j.ImportDeclaration, {
57
+ source: { value: "@planningcenter/tapestry-react" },
58
+ }),
59
+ "Divider"
60
+ )
61
+ }
62
+ }
63
+
64
+ return updatedSource.toSource()
65
+ }
66
+
67
+ export default transform
@@ -205,7 +205,7 @@ export default function Test() {
205
205
  expect(result).not.toContain("Dropdown.Item")
206
206
  })
207
207
 
208
- it("does not touch the import statement", () => {
208
+ it("does not touch the tapestry-react import statement", () => {
209
209
  const input = `
210
210
  import { Dropdown } from "@planningcenter/tapestry-react"
211
211
 
@@ -223,6 +223,24 @@ export default function Test() {
223
223
  'import { Dropdown } from "@planningcenter/tapestry-react"'
224
224
  )
225
225
  })
226
+
227
+ it("adds DropdownAction import from tapestry", () => {
228
+ const input = `
229
+ import { Dropdown } from "@planningcenter/tapestry-react"
230
+
231
+ export default function Test() {
232
+ return (
233
+ <Dropdown title="Actions">
234
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
235
+ </Dropdown>
236
+ )
237
+ }
238
+ `.trim()
239
+
240
+ const result = applyTransform(input)
241
+ expect(result).toContain('from "@planningcenter/tapestry"')
242
+ expect(result).toContain("DropdownAction")
243
+ })
226
244
  })
227
245
 
228
246
  describe("multiple items", () => {
@@ -3,11 +3,12 @@ import { Transform } from "jscodeshift"
3
3
  import { transformAttributeName } from "../../shared/actions/transformAttributeName"
4
4
  import { transformElementName } from "../../shared/actions/transformElementName"
5
5
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
6
7
 
7
8
  const transform: Transform = attributeTransformFactory({
8
9
  targetComponent: "Dropdown.Item",
9
10
  targetPackage: "@planningcenter/tapestry-react",
10
- transform: (element, { j, options }) => {
11
+ transform: (element, { j, options, source }) => {
11
12
  const onSelectRenamed = transformAttributeName("onSelect", "onAction", {
12
13
  element,
13
14
  j,
@@ -27,7 +28,18 @@ const transform: Transform = attributeTransformFactory({
27
28
  element,
28
29
  name: "DropdownAction",
29
30
  })
30
- return onSelectRenamed || valueRenamed || textRenamed || elementRenamed
31
+ const changed =
32
+ onSelectRenamed || valueRenamed || textRenamed || elementRenamed
33
+ if (changed) {
34
+ addImport({
35
+ component: "DropdownAction",
36
+ fromPackage: "@planningcenter/tapestry-react",
37
+ j,
38
+ pkg: "@planningcenter/tapestry",
39
+ source,
40
+ })
41
+ }
42
+ return changed
31
43
  },
32
44
  })
33
45
 
@@ -186,7 +186,7 @@ export default function Test() {
186
186
  expect(result).toContain("<DropdownLink")
187
187
  })
188
188
 
189
- it("does not touch the import statement", () => {
189
+ it("does not touch the tapestry-react import statement", () => {
190
190
  const input = `
191
191
  import { Dropdown } from "@planningcenter/tapestry-react"
192
192
 
@@ -204,6 +204,24 @@ export default function Test() {
204
204
  'import { Dropdown } from "@planningcenter/tapestry-react"'
205
205
  )
206
206
  })
207
+
208
+ it("adds DropdownLink import from tapestry", () => {
209
+ const input = `
210
+ import { Dropdown } from "@planningcenter/tapestry-react"
211
+
212
+ export default function Test() {
213
+ return (
214
+ <Dropdown title="Nav">
215
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
216
+ </Dropdown>
217
+ )
218
+ }
219
+ `.trim()
220
+
221
+ const result = applyTransform(input)
222
+ expect(result).toContain('from "@planningcenter/tapestry"')
223
+ expect(result).toContain("DropdownLink")
224
+ })
207
225
  })
208
226
 
209
227
  describe("no changes scenarios", () => {
@@ -3,11 +3,12 @@ import { Transform } from "jscodeshift"
3
3
  import { transformAttributeName } from "../../shared/actions/transformAttributeName"
4
4
  import { transformElementName } from "../../shared/actions/transformElementName"
5
5
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
6
7
 
7
8
  const transform: Transform = attributeTransformFactory({
8
9
  targetComponent: "Dropdown.Link",
9
10
  targetPackage: "@planningcenter/tapestry-react",
10
- transform: (element, { j, options }) => {
11
+ transform: (element, { j, options, source }) => {
11
12
  const toRenamed = transformAttributeName("to", "href", {
12
13
  element,
13
14
  j,
@@ -17,7 +18,17 @@ const transform: Transform = attributeTransformFactory({
17
18
  element,
18
19
  name: "DropdownLink",
19
20
  })
20
- return toRenamed || elementRenamed
21
+ const changed = toRenamed || elementRenamed
22
+ if (changed) {
23
+ addImport({
24
+ component: "DropdownLink",
25
+ fromPackage: "@planningcenter/tapestry-react",
26
+ j,
27
+ pkg: "@planningcenter/tapestry",
28
+ source,
29
+ })
30
+ }
31
+ return changed
21
32
  },
22
33
  })
23
34
 
@@ -0,0 +1,93 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./moveDropdownImport"
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("moveDropdownImport transform", () => {
18
+ it("moves Dropdown import from tapestry-react to tapestry", () => {
19
+ const input = `
20
+ import { Dropdown } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).toContain(
29
+ 'import { Dropdown } from "@planningcenter/tapestry"'
30
+ )
31
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
32
+ })
33
+
34
+ it("merges Dropdown into an existing tapestry import", () => {
35
+ const input = `
36
+ import { Dropdown } from "@planningcenter/tapestry-react"
37
+ import { DropdownAction, DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
38
+
39
+ export default function Test() {
40
+ return (
41
+ <Dropdown onClose={fn}>
42
+ <DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
43
+ <DropdownMenu><DropdownAction onAction={fn}>Edit</DropdownAction></DropdownMenu>
44
+ </Dropdown>
45
+ )
46
+ }
47
+ `.trim()
48
+
49
+ const result = applyTransform(input)
50
+ expect(result).toContain("Dropdown")
51
+ expect(result).toContain("DropdownAction")
52
+ expect(result).toContain("DropdownMenu")
53
+ expect(result).toContain("DropdownTrigger")
54
+ expect(result).toContain('from "@planningcenter/tapestry"')
55
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
56
+ })
57
+
58
+ it("preserves other tapestry-react imports", () => {
59
+ const input = `
60
+ import { Dropdown, Button } from "@planningcenter/tapestry-react"
61
+
62
+ export default function Test() {
63
+ return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
64
+ }
65
+ `.trim()
66
+
67
+ const result = applyTransform(input)
68
+ expect(result).toContain(
69
+ 'import { Button } from "@planningcenter/tapestry-react"'
70
+ )
71
+ expect(result).toContain(
72
+ 'import { Dropdown } from "@planningcenter/tapestry"'
73
+ )
74
+ })
75
+
76
+ describe("no changes scenarios", () => {
77
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
78
+ const input = `
79
+ import { Dropdown } from "@planningcenter/tapestry"
80
+
81
+ export default function Test() {
82
+ return <Dropdown onClose={fn}><DropdownMenu /></Dropdown>
83
+ }
84
+ `.trim()
85
+
86
+ expect(applyTransform(input)).toBe(null)
87
+ })
88
+
89
+ it("returns null for empty file", () => {
90
+ expect(applyTransform("")).toBe(null)
91
+ })
92
+ })
93
+ })
@@ -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: "Dropdown",
8
+ fromPackage: "@planningcenter/tapestry-react",
9
+ toComponent: "Dropdown",
10
+ toPackage: "@planningcenter/tapestry",
11
+ })
12
+
13
+ export default transform
@@ -0,0 +1,135 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./placementIdToMenu"
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
+ const withMenu = (
18
+ attrs: string,
19
+ items = "<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>"
20
+ ) =>
21
+ `
22
+ import { Dropdown } from "@planningcenter/tapestry-react"
23
+
24
+ export default function Test() {
25
+ return (
26
+ <Dropdown ${attrs}>
27
+ <DropdownMenu>
28
+ ${items}
29
+ </DropdownMenu>
30
+ </Dropdown>
31
+ )
32
+ }
33
+ `.trim()
34
+
35
+ describe("placementIdToMenu transform", () => {
36
+ describe("placement", () => {
37
+ it("moves placement string literal to DropdownMenu with dash-to-space conversion", () => {
38
+ const result = applyTransform(withMenu('placement="bottom-start"'))
39
+ expect(result).toContain('<DropdownMenu placement="bottom start"')
40
+ expect(result).not.toMatch(/<Dropdown\s[^>]*placement/)
41
+ })
42
+
43
+ it("converts all dash-separated placement values", () => {
44
+ const result = applyTransform(withMenu('placement="top-end"'))
45
+ expect(result).toContain('<DropdownMenu placement="top end"')
46
+ })
47
+
48
+ it("moves placement from JSXExpressionContainer string literal", () => {
49
+ const result = applyTransform(withMenu('placement={"bottom-end"}'))
50
+ expect(result).toContain('<DropdownMenu placement={"bottom end"}')
51
+ })
52
+
53
+ it("moves dynamic placement expression and adds TODO comment", () => {
54
+ const result = applyTransform(withMenu("placement={myPlacement}"))
55
+ expect(result).toContain("<DropdownMenu placement={myPlacement}")
56
+ expect(result).toContain(
57
+ "TODO: tapestry-migration (placement): placement is a dynamic expression"
58
+ )
59
+ })
60
+ })
61
+
62
+ describe("id", () => {
63
+ it("moves id string literal to DropdownMenu", () => {
64
+ const result = applyTransform(withMenu('id="my-menu"'))
65
+ expect(result).toContain('<DropdownMenu id="my-menu"')
66
+ expect(result).not.toMatch(/<Dropdown\s[^>]*\bid=/)
67
+ })
68
+
69
+ it("moves dynamic id expression to DropdownMenu", () => {
70
+ const result = applyTransform(withMenu("id={menuId}"))
71
+ expect(result).toContain("<DropdownMenu id={menuId}")
72
+ })
73
+ })
74
+
75
+ describe("placement and id together", () => {
76
+ it("moves both placement and id to DropdownMenu", () => {
77
+ const result = applyTransform(
78
+ withMenu('placement="bottom-start" id="my-menu"')
79
+ )
80
+ expect(result).toContain("placement=")
81
+ expect(result).toContain("id=")
82
+ expect(result).toContain("<DropdownMenu")
83
+ expect(result).not.toMatch(/<Dropdown\s[^>]*placement/)
84
+ expect(result).not.toMatch(/<Dropdown\s[^>]*\bid=/)
85
+ })
86
+ })
87
+
88
+ describe("no changes scenarios", () => {
89
+ it("returns null when neither placement nor id is present", () => {
90
+ const result = applyTransform(withMenu("onClose={handleClose}"))
91
+ expect(result).toBe(null)
92
+ })
93
+
94
+ it("adds TODO when DropdownMenu child is not found", () => {
95
+ const input = `
96
+ import { Dropdown } from "@planningcenter/tapestry-react"
97
+
98
+ export default function Test() {
99
+ return <Dropdown placement="bottom-start">{children}</Dropdown>
100
+ }
101
+ `.trim()
102
+
103
+ const result = applyTransform(input)
104
+ expect(result).toContain("TODO: tapestry-migration (placement)")
105
+ expect(result).toContain("no DropdownMenu child found")
106
+ })
107
+
108
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
109
+ const input = `
110
+ import { Dropdown } from "some-other-library"
111
+
112
+ export default function Test() {
113
+ return (
114
+ <Dropdown placement="bottom-start">
115
+ <DropdownMenu><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></DropdownMenu>
116
+ </Dropdown>
117
+ )
118
+ }
119
+ `.trim()
120
+
121
+ expect(applyTransform(input)).toBe(null)
122
+ })
123
+
124
+ it("does not touch the import statement", () => {
125
+ const result = applyTransform(withMenu('placement="bottom-start"'))
126
+ expect(result).toContain(
127
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
128
+ )
129
+ })
130
+
131
+ it("returns null for empty file", () => {
132
+ expect(applyTransform("")).toBe(null)
133
+ })
134
+ })
135
+ })
@@ -0,0 +1,96 @@
1
+ import { JSXAttribute, JSXElement, Transform } from "jscodeshift"
2
+
3
+ import { addComment } from "../../shared/actions/addComment"
4
+ import { getAttribute } from "../../shared/actions/getAttribute"
5
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
6
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
7
+
8
+ function findDropdownMenu(element: JSXElement): JSXElement | undefined {
9
+ return (element.children ?? []).find(
10
+ (child): child is JSXElement =>
11
+ child.type === "JSXElement" &&
12
+ child.openingElement.name.type === "JSXIdentifier" &&
13
+ child.openingElement.name.name === "DropdownMenu"
14
+ )
15
+ }
16
+
17
+ function applyPlacementConversion(attr: JSXAttribute): { isDynamic: boolean } {
18
+ const val = attr.value
19
+ if (val?.type === "StringLiteral") {
20
+ val.value = val.value.replace(/-/g, " ")
21
+ return { isDynamic: false }
22
+ }
23
+ if (val?.type === "JSXExpressionContainer") {
24
+ if (val.expression.type === "StringLiteral") {
25
+ val.expression.value = val.expression.value.replace(/-/g, " ")
26
+ return { isDynamic: false }
27
+ }
28
+ if (val.expression.type !== "JSXEmptyExpression") {
29
+ return { isDynamic: true }
30
+ }
31
+ }
32
+ return { isDynamic: false }
33
+ }
34
+
35
+ function moveAttr(
36
+ name: string,
37
+ attr: JSXAttribute,
38
+ from: JSXElement,
39
+ to: JSXElement,
40
+ {
41
+ j,
42
+ source,
43
+ }: {
44
+ j: Parameters<typeof removeAttribute>[1]["j"]
45
+ source: Parameters<typeof removeAttribute>[1]["source"]
46
+ }
47
+ ) {
48
+ to.openingElement.attributes = [...(to.openingElement.attributes ?? []), attr]
49
+ removeAttribute(name, { element: from, j, source })
50
+ }
51
+
52
+ const transform: Transform = attributeTransformFactory({
53
+ targetComponent: "Dropdown",
54
+ targetPackage: "@planningcenter/tapestry-react",
55
+ transform: (element, { j, source }) => {
56
+ const placementAttr = getAttribute({ element, name: "placement" })
57
+ const idAttr = getAttribute({ element, name: "id" })
58
+
59
+ if (!placementAttr && !idAttr) return false
60
+
61
+ const menuChild = findDropdownMenu(element)
62
+
63
+ if (!menuChild) {
64
+ addComment({
65
+ element,
66
+ j,
67
+ scope: "placement",
68
+ source,
69
+ text: "placement/id could not be moved to DropdownMenu — no DropdownMenu child found",
70
+ })
71
+ return true
72
+ }
73
+
74
+ if (idAttr) {
75
+ moveAttr("id", idAttr, element, menuChild, { j, source })
76
+ }
77
+
78
+ if (placementAttr) {
79
+ const { isDynamic } = applyPlacementConversion(placementAttr)
80
+ moveAttr("placement", placementAttr, element, menuChild, { j, source })
81
+ if (isDynamic) {
82
+ addComment({
83
+ element,
84
+ j,
85
+ scope: "placement",
86
+ source,
87
+ text: "placement is a dynamic expression — dash-to-space conversion (e.g. 'bottom-start' → 'bottom start') must be done manually",
88
+ })
89
+ }
90
+ }
91
+
92
+ return true
93
+ },
94
+ })
95
+
96
+ export default transform
@@ -0,0 +1,145 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./unsupportedProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return (
11
+ (transform(
12
+ fileInfo,
13
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
14
+ {}
15
+ ) as string | null) || source
16
+ )
17
+ }
18
+
19
+ describe("unsupportedProps transform", () => {
20
+ describe("flags removed props", () => {
21
+ it("flags variant", () => {
22
+ const input = `
23
+ import { Dropdown } from "@planningcenter/tapestry-react"
24
+
25
+ export default function Test() {
26
+ return <Dropdown variant="outline" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
27
+ }
28
+ `.trim()
29
+
30
+ const result = applyTransform(input)
31
+ expect(result).toContain("TODO: tapestry-migration (variant)")
32
+ expect(result).toContain("variant")
33
+ })
34
+
35
+ it("flags theme", () => {
36
+ const input = `
37
+ import { Dropdown } from "@planningcenter/tapestry-react"
38
+
39
+ export default function Test() {
40
+ return <Dropdown theme="primary" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
41
+ }
42
+ `.trim()
43
+
44
+ const result = applyTransform(input)
45
+ expect(result).toContain("TODO: tapestry-migration (theme)")
46
+ })
47
+
48
+ it("flags size", () => {
49
+ const input = `
50
+ import { Dropdown } from "@planningcenter/tapestry-react"
51
+
52
+ export default function Test() {
53
+ return <Dropdown size="md" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
54
+ }
55
+ `.trim()
56
+
57
+ const result = applyTransform(input)
58
+ expect(result).toContain("TODO: tapestry-migration (size)")
59
+ })
60
+
61
+ it("flags popoverProps", () => {
62
+ const input = `
63
+ import { Dropdown } from "@planningcenter/tapestry-react"
64
+
65
+ export default function Test() {
66
+ return <Dropdown popoverProps={{ offset: 8 }} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
67
+ }
68
+ `.trim()
69
+
70
+ const result = applyTransform(input)
71
+ expect(result).toContain("TODO: tapestry-migration (popoverProps)")
72
+ })
73
+
74
+ it("flags multiple unsupported props", () => {
75
+ const input = `
76
+ import { Dropdown } from "@planningcenter/tapestry-react"
77
+
78
+ export default function Test() {
79
+ return <Dropdown variant="outline" theme="primary" css={{ color: "red" }} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
80
+ }
81
+ `.trim()
82
+
83
+ const result = applyTransform(input)
84
+ expect(result).toContain("TODO: tapestry-migration (variant)")
85
+ expect(result).toContain("TODO: tapestry-migration (theme)")
86
+ expect(result).toContain("TODO: tapestry-migration (css)")
87
+ })
88
+ })
89
+
90
+ describe("does not flag supported props", () => {
91
+ it("does not flag onClose or onOpen", () => {
92
+ const input = `
93
+ import { Dropdown } from "@planningcenter/tapestry-react"
94
+
95
+ export default function Test() {
96
+ return <Dropdown onClose={handleClose} onOpen={handleOpen}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
97
+ }
98
+ `.trim()
99
+
100
+ expect(applyTransform(input)).toBe(input)
101
+ })
102
+
103
+ it("does not flag className or ref", () => {
104
+ const input = `
105
+ import { Dropdown } from "@planningcenter/tapestry-react"
106
+
107
+ export default function Test() {
108
+ return <Dropdown className="my-dropdown" ref={ref} onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
109
+ }
110
+ `.trim()
111
+
112
+ expect(applyTransform(input)).toBe(input)
113
+ })
114
+
115
+ it("does not flag aria- and data- attributes", () => {
116
+ const input = `
117
+ import { Dropdown } from "@planningcenter/tapestry-react"
118
+
119
+ export default function Test() {
120
+ return <Dropdown aria-label="Options" data-pendo="dropdown" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
121
+ }
122
+ `.trim()
123
+
124
+ expect(applyTransform(input)).toBe(input)
125
+ })
126
+ })
127
+
128
+ describe("no changes scenarios", () => {
129
+ it("returns source unchanged when not from tapestry-react", () => {
130
+ const input = `
131
+ import { Dropdown } from "some-other-library"
132
+
133
+ export default function Test() {
134
+ return <Dropdown variant="outline"><Dropdown.Item>Edit</Dropdown.Item></Dropdown>
135
+ }
136
+ `.trim()
137
+
138
+ expect(applyTransform(input)).toBe(input)
139
+ })
140
+
141
+ it("returns source unchanged for empty file", () => {
142
+ expect(applyTransform("")).toBe("")
143
+ })
144
+ })
145
+ })