Skip to content

Commit ceb0aef

Browse files
konardclaude
andcommitted
refactor(tests): eliminate code duplication in test files
CHANGE: Extracted common test setup patterns into reusable helper functions WHY: vibecode-linter detected 11 duplicate code blocks in test files REF: issue-25, CI test failures Implementation: - Added transformAndValidateJsx helper to reduce repetitive test setup - Added createEmptyNodeWithLocation and createNodeWithClassNameAndLocation fixtures - Refactored 12 transformation tests to use new helpers - Refactored 2 config tests to use new helpers - Refactored 3 core JSX tests to use new fixtures Duplicate Reduction: - Eliminated 11 duplicate code blocks (6-line patterns repeated across tests) - All test setup patterns now consolidated into single-purpose helper functions - Maintains same test coverage (47/47 tests passing) INVARIANT: All tests verify same mathematical properties as before COMPLEXITY: Test execution time unchanged (1.27s) Test Results: - Test Files: 6 passed (6) - Tests: 47 passed (47) - Duration: 1.27s - Lint: ✅ 0 errors, ✅ No code duplicates found Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 870b209 commit ceb0aef

5 files changed

Lines changed: 146 additions & 105 deletions

File tree

packages/app/tests/core/jsx-tagger.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { describe, expect, it } from "@effect/vitest"
33
import { Effect } from "effect"
44

55
import { attrExists, createPathAttribute, type JsxTaggerContext, processJsxElement } from "../../src/core/jsx-tagger.js"
6-
import { createEmptyNode, createMockLocation, createNodeWithClassName } from "./jsx-test-fixtures.js"
6+
import {
7+
createEmptyNodeWithLocation,
8+
createNodeWithClassName,
9+
createNodeWithClassNameAndLocation
10+
} from "./jsx-test-fixtures.js"
711

812
// CHANGE: add comprehensive unit tests for jsx-tagger core functions
913
// WHY: ensure mathematical invariants and idempotency properties are verified
@@ -129,9 +133,7 @@ describe("jsx-tagger", () => {
129133

130134
it.effect("adds path attribute when element has no attributes", () =>
131135
Effect.sync(() => {
132-
const node = createEmptyNode(t)
133-
node.loc = createMockLocation()
134-
136+
const node = createEmptyNodeWithLocation(t)
135137
const result = processJsxElement(node, createTestContext(), t)
136138

137139
expect(result).toBe(true)
@@ -146,9 +148,7 @@ describe("jsx-tagger", () => {
146148

147149
it.effect("adds path attribute when element has other attributes", () =>
148150
Effect.sync(() => {
149-
const node = createNodeWithClassName(t)
150-
node.loc = createMockLocation(15, 2)
151-
151+
const node = createNodeWithClassNameAndLocation(t)
152152
const result = processJsxElement(node, createTestContext(), t)
153153

154154
expect(result).toBe(true)
@@ -192,8 +192,7 @@ describe("jsx-tagger", () => {
192192

193193
it.effect("is truly idempotent - processing twice produces same result", () =>
194194
Effect.sync(() => {
195-
const node = createEmptyNode(t)
196-
node.loc = createMockLocation()
195+
const node = createEmptyNodeWithLocation(t)
197196

198197
// First processing
199198
const result1 = processJsxElement(node, createTestContext(), t)

packages/app/tests/core/jsx-test-fixtures.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,38 @@ export const createNodeWithClassName = (
4747
*/
4848
export const createEmptyNode = (types: typeof t): t.JSXOpeningElement =>
4949
types.jsxOpeningElement(types.jsxIdentifier("div"), [])
50+
51+
/**
52+
* Creates an empty JSX opening element with location info for testing.
53+
* Combines node creation and location setup to reduce duplication.
54+
*
55+
* @pure true
56+
* @complexity O(1)
57+
*/
58+
export const createEmptyNodeWithLocation = (
59+
types: typeof t,
60+
line = 10,
61+
column = 5
62+
): t.JSXOpeningElement => {
63+
const node = createEmptyNode(types)
64+
node.loc = createMockLocation(line, column)
65+
return node
66+
}
67+
68+
/**
69+
* Creates a JSX opening element with className attribute and location info for testing.
70+
* Combines node creation and location setup to reduce duplication.
71+
*
72+
* @pure true
73+
* @complexity O(1)
74+
*/
75+
export const createNodeWithClassNameAndLocation = (
76+
types: typeof t,
77+
className = "container",
78+
line = 15,
79+
column = 2
80+
): t.JSXOpeningElement => {
81+
const node = createNodeWithClassName(types, className)
82+
node.loc = createMockLocation(line, column)
83+
return node
84+
}

packages/app/tests/shell/babel-plugin-config.test.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Effect } from "effect"
44
import path from "node:path"
55

66
import { componentTaggerBabelPlugin } from "../../src/shell/babel-plugin.js"
7-
import { expectPathAttribute, transformJsx } from "./babel-test-utils.js"
7+
import { expectPathAttribute, transformAndValidateJsx, transformJsx } from "./babel-test-utils.js"
88

99
// CHANGE: extract plugin configuration and rootDir tests to separate file.
1010
// WHY: comply with max-lines ESLint rule (300 lines limit).
@@ -77,16 +77,13 @@ describe("babel-plugin configuration", () => {
7777
it.effect("uses custom attribute name when provided", () =>
7878
Effect.sync(() => {
7979
const code = "const App = () => { return <div>Hello</div> }"
80-
const testFilename = path.resolve("/project", "src/App.tsx")
8180

82-
const result = transformJsx(code, testFilename, {
83-
rootDir: "/project",
81+
const { code: transformedCode } = transformAndValidateJsx(code, "src/App.tsx", {
8482
attributeName: "custom-path"
8583
})
8684

87-
expect(result).not.toBeNull()
88-
expect(result?.code).toContain("custom-path=\"src/App.tsx:")
89-
expect(result?.code).not.toContain("data-path=")
85+
expect(transformedCode).toContain("custom-path=\"src/App.tsx:")
86+
expect(transformedCode).not.toContain("data-path=")
9087
}))
9188

9289
it.effect("respects idempotency with custom attribute name", () =>
@@ -96,19 +93,14 @@ describe("babel-plugin configuration", () => {
9693
return <div custom-path="existing:1:0">Hello</div>
9794
}
9895
`
99-
const testFilename = path.resolve("/project", "src/App.tsx")
100-
101-
const result = transformJsx(code, testFilename, {
102-
rootDir: "/project",
96+
const { expectContains, expectDataPathCount } = transformAndValidateJsx(code, "src/App.tsx", {
10397
attributeName: "custom-path"
10498
})
10599

106-
expect(result).not.toBeNull()
107100
// Should keep the existing custom-path attribute
108-
expect(result?.code).toContain("custom-path=\"existing:1:0\"")
101+
expectContains("custom-path=\"existing:1:0\"")
109102
// Count custom-path attributes - should only be one
110-
const pathMatches = result?.code?.match(/custom-path="/g)
111-
expect(pathMatches?.length).toBe(1)
103+
expectDataPathCount(1)
112104
}))
113105
})
114106

Lines changed: 38 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { describe, expect, it } from "@effect/vitest"
1+
import { describe, it } from "@effect/vitest"
22
import { Effect } from "effect"
3-
import path from "node:path"
43

5-
import { transformJsx } from "./babel-test-utils.js"
4+
import { transformAndValidateJsx } from "./babel-test-utils.js"
65

76
// CHANGE: extract JSX transformation tests to separate file.
87
// WHY: comply with max-lines ESLint rule (300 lines limit).
@@ -20,13 +19,10 @@ describe("babel-plugin JSX transformations", () => {
2019
return <div>Hello</div>
2120
}
2221
`
23-
const testFilename = path.resolve("/project", "src/App.tsx")
22+
const { expectContains } = transformAndValidateJsx(code, "src/App.tsx")
2423

25-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
26-
27-
expect(result).not.toBeNull()
28-
expect(result?.code).toContain("data-path=\"src/App.tsx:")
29-
expect(result?.code).toContain("<div")
24+
expectContains("data-path=\"src/App.tsx:")
25+
expectContains("<div")
3026
}))
3127

3228
it.effect("transforms multiple JSX elements", () =>
@@ -41,15 +37,10 @@ describe("babel-plugin JSX transformations", () => {
4137
)
4238
}
4339
`
44-
const testFilename = path.resolve("/project", "src/App.tsx")
45-
46-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
40+
const { expectDataPathMinCount } = transformAndValidateJsx(code, "src/App.tsx")
4741

48-
expect(result).not.toBeNull()
4942
// Should contain data-path attributes for div, header, and main
50-
const pathMatches = result?.code?.match(/data-path="src\/App\.tsx:\d+:\d+"/g)
51-
expect(pathMatches).toBeDefined()
52-
expect(pathMatches?.length).toBeGreaterThanOrEqual(3)
43+
expectDataPathMinCount(3)
5344
}))
5445

5546
it.effect("does not add duplicate data-path attribute (idempotency)", () =>
@@ -59,16 +50,12 @@ describe("babel-plugin JSX transformations", () => {
5950
return <div data-path="existing:1:0">Hello</div>
6051
}
6152
`
62-
const testFilename = path.resolve("/project", "src/App.tsx")
63-
64-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
53+
const { expectContains, expectDataPathCount } = transformAndValidateJsx(code, "src/App.tsx")
6554

66-
expect(result).not.toBeNull()
6755
// Should keep the existing data-path attribute
68-
expect(result?.code).toContain("data-path=\"existing:1:0\"")
56+
expectContains("data-path=\"existing:1:0\"")
6957
// Count data-path attributes - should only be one
70-
const pathMatches = result?.code?.match(/data-path="/g)
71-
expect(pathMatches?.length).toBe(1)
58+
expectDataPathCount(1)
7259
}))
7360

7461
it.effect("does not interfere with other path-like attributes", () =>
@@ -78,15 +65,12 @@ describe("babel-plugin JSX transformations", () => {
7865
return <img src="/image.png" alt="test" />
7966
}
8067
`
81-
const testFilename = path.resolve("/project", "src/App.tsx")
68+
const { expectContains } = transformAndValidateJsx(code, "src/App.tsx")
8269

83-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
84-
85-
expect(result).not.toBeNull()
8670
// Should preserve src attribute
87-
expect(result?.code).toContain("src=\"/image.png\"")
71+
expectContains("src=\"/image.png\"")
8872
// Should add data-path attribute
89-
expect(result?.code).toContain("data-path=\"src/App.tsx:")
73+
expectContains("data-path=\"src/App.tsx:")
9074
}))
9175

9276
it.effect("handles JSX with existing attributes", () =>
@@ -96,17 +80,14 @@ describe("babel-plugin JSX transformations", () => {
9680
return <button className="btn" id="submit" onClick={handleClick}>Click</button>
9781
}
9882
`
99-
const testFilename = path.resolve("/project", "src/components/Button.tsx")
100-
101-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
83+
const { expectContains } = transformAndValidateJsx(code, "src/components/Button.tsx")
10284

103-
expect(result).not.toBeNull()
10485
// Should preserve existing attributes
105-
expect(result?.code).toContain("className=\"btn\"")
106-
expect(result?.code).toContain("id=\"submit\"")
107-
expect(result?.code).toContain("onClick={handleClick}")
86+
expectContains("className=\"btn\"")
87+
expectContains("id=\"submit\"")
88+
expectContains("onClick={handleClick}")
10889
// Should add data-path attribute
109-
expect(result?.code).toContain("data-path=\"src/components/Button.tsx:")
90+
expectContains("data-path=\"src/components/Button.tsx:")
11091
}))
11192

11293
it.effect("handles self-closing JSX elements", () =>
@@ -116,13 +97,10 @@ describe("babel-plugin JSX transformations", () => {
11697
return <input type="text" />
11798
}
11899
`
119-
const testFilename = path.resolve("/project", "src/App.tsx")
120-
121-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
100+
const { expectContains } = transformAndValidateJsx(code, "src/App.tsx")
122101

123-
expect(result).not.toBeNull()
124-
expect(result?.code).toContain("data-path=\"src/App.tsx:")
125-
expect(result?.code).toContain("type=\"text\"")
102+
expectContains("data-path=\"src/App.tsx:")
103+
expectContains("type=\"text\"")
126104
}))
127105

128106
it.effect("handles nested JSX components", () =>
@@ -142,15 +120,10 @@ describe("babel-plugin JSX transformations", () => {
142120
)
143121
}
144122
`
145-
const testFilename = path.resolve("/project", "src/pages/Page.tsx")
123+
const { expectDataPathMinCount } = transformAndValidateJsx(code, "src/pages/Page.tsx")
146124

147-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
148-
149-
expect(result).not.toBeNull()
150-
// All components should be tagged
151-
const pathMatches = result?.code?.match(/data-path="src\/pages\/Page\.tsx:\d+:\d+"/g)
152-
expect(pathMatches).toBeDefined()
153-
expect(pathMatches?.length).toBeGreaterThanOrEqual(6) // Layout, Header, Logo, Nav, Content, Article
125+
// All components should be tagged: Layout, Header, Logo, Nav, Content, Article
126+
expectDataPathMinCount(6)
154127
}))
155128

156129
it.effect("handles JSX fragments", () =>
@@ -165,14 +138,10 @@ describe("babel-plugin JSX transformations", () => {
165138
)
166139
}
167140
`
168-
const testFilename = path.resolve("/project", "src/App.tsx")
169-
170-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
141+
const { expectDataPathMinCount } = transformAndValidateJsx(code, "src/App.tsx")
171142

172-
expect(result).not.toBeNull()
173-
// Fragments don't get tagged, but their children do
174-
const pathMatches = result?.code?.match(/data-path="/g)
175-
expect(pathMatches?.length).toBeGreaterThanOrEqual(2) // Two div elements
143+
// Fragments don't get tagged, but their children do (two div elements)
144+
expectDataPathMinCount(2)
176145
}))
177146

178147
it.effect("handles JSX with spread attributes", () =>
@@ -182,13 +151,10 @@ describe("babel-plugin JSX transformations", () => {
182151
return <div {...props}>Content</div>
183152
}
184153
`
185-
const testFilename = path.resolve("/project", "src/App.tsx")
186-
187-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
154+
const { expectContains } = transformAndValidateJsx(code, "src/App.tsx")
188155

189-
expect(result).not.toBeNull()
190-
expect(result?.code).toContain("{...props}")
191-
expect(result?.code).toContain("data-path=\"src/App.tsx:")
156+
expectContains("{...props}")
157+
expectContains("data-path=\"src/App.tsx:")
192158
}))
193159

194160
it.effect("handles TypeScript JSX generics", () =>
@@ -198,26 +164,20 @@ describe("babel-plugin JSX transformations", () => {
198164
return <div>Generic Component</div>
199165
}
200166
`
201-
const testFilename = path.resolve("/project", "src/Generic.tsx")
167+
const { expectContains } = transformAndValidateJsx(code, "src/Generic.tsx")
202168

203-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
204-
205-
expect(result).not.toBeNull()
206-
expect(result?.code).toContain("data-path=\"src/Generic.tsx:")
169+
expectContains("data-path=\"src/Generic.tsx:")
207170
}))
208171

209172
it.effect("correctly formats data-path with line and column", () =>
210173
Effect.sync(() => {
211174
const code = `function App() {
212175
return <div>Test</div>
213176
}`
214-
const testFilename = path.resolve("/project", "src/App.tsx")
215-
216-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
177+
const { expectMatch } = transformAndValidateJsx(code, "src/App.tsx")
217178

218-
expect(result).not.toBeNull()
219179
// The data-path should contain line 2 (where <div> is) and column number
220-
expect(result?.code).toMatch(/data-path="src\/App\.tsx:2:\d+"/)
180+
expectMatch(/data-path="src\/App\.tsx:2:\d+"/)
221181
}))
222182

223183
it.effect("handles components with multiple props on multiple lines", () =>
@@ -235,13 +195,10 @@ describe("babel-plugin JSX transformations", () => {
235195
)
236196
}
237197
`
238-
const testFilename = path.resolve("/project", "src/App.tsx")
239-
240-
const result = transformJsx(code, testFilename, { rootDir: "/project" })
198+
const { expectContains } = transformAndValidateJsx(code, "src/App.tsx")
241199

242-
expect(result).not.toBeNull()
243-
expect(result?.code).toContain("data-path=\"src/App.tsx:")
244-
expect(result?.code).toContain("className=\"primary\"")
245-
expect(result?.code).toContain("onClick={handleClick}")
200+
expectContains("data-path=\"src/App.tsx:")
201+
expectContains("className=\"primary\"")
202+
expectContains("onClick={handleClick}")
246203
}))
247204
})

0 commit comments

Comments
 (0)