Skip to content

Commit d56740c

Browse files
committed
Merge origin/main into billing-di-refactor-v2
2 parents 9c2a4d2 + 0642a2e commit d56740c

File tree

9 files changed

+390
-232
lines changed

9 files changed

+390
-232
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2+
3+
import {
4+
getCopyIconText,
5+
copyButtonHandlers,
6+
COPIED_RESET_DELAY_MS,
7+
COPY_ICON_COLLAPSED,
8+
COPY_ICON_EXPANDED,
9+
COPY_ICON_COPIED,
10+
} from '../../components/copy-button'
11+
import { initializeThemeStore } from '../../hooks/use-theme'
12+
13+
// Initialize theme before tests
14+
initializeThemeStore()
15+
16+
/**
17+
* Tests for CopyButton component logic.
18+
*
19+
* These tests use the exported utilities from copy-button.tsx:
20+
* - getCopyIconText: determines what text to display
21+
* - copyButtonHandlers: pure functions for state transitions
22+
* - COPIED_RESET_DELAY_MS: the timeout constant
23+
*/
24+
25+
describe('CopyButton - CopyIcon text rendering', () => {
26+
describe('with leadingSpace=true (default)', () => {
27+
test('renders collapsed icon when not hovered or copied', () => {
28+
const text = getCopyIconText(false, false, true)
29+
expect(text).toBe(' ⎘')
30+
})
31+
32+
test('renders expanded text when hovered', () => {
33+
const text = getCopyIconText(false, true, true)
34+
expect(text).toBe(' [⎘ copy]')
35+
})
36+
37+
test('renders copied text when copied', () => {
38+
const text = getCopyIconText(true, false, true)
39+
expect(text).toBe(' [✔ copied]')
40+
})
41+
42+
test('renders copied text even when hovered (copied takes priority)', () => {
43+
const text = getCopyIconText(true, true, true)
44+
expect(text).toBe(' [✔ copied]')
45+
})
46+
})
47+
48+
describe('with leadingSpace=false', () => {
49+
test('renders collapsed icon without leading space', () => {
50+
const text = getCopyIconText(false, false, false)
51+
expect(text).toBe('⎘')
52+
})
53+
54+
test('renders expanded text without leading space', () => {
55+
const text = getCopyIconText(false, true, false)
56+
expect(text).toBe('[⎘ copy]')
57+
})
58+
59+
test('renders copied text without leading space', () => {
60+
const text = getCopyIconText(true, false, false)
61+
expect(text).toBe('[✔ copied]')
62+
})
63+
})
64+
})
65+
66+
describe('CopyButton - copyButtonHandlers (from component)', () => {
67+
describe('handleMouseOver', () => {
68+
test('returns true (should hover) when not copied', () => {
69+
expect(copyButtonHandlers.handleMouseOver(false)).toBe(true)
70+
})
71+
72+
test('returns false (block hover) when copied', () => {
73+
expect(copyButtonHandlers.handleMouseOver(true)).toBe(false)
74+
})
75+
})
76+
77+
describe('handleMouseOut', () => {
78+
test('always returns false to clear hover', () => {
79+
expect(copyButtonHandlers.handleMouseOut()).toBe(false)
80+
})
81+
})
82+
83+
describe('handleCopy', () => {
84+
test('returns copied=true and clears hover', () => {
85+
const result = copyButtonHandlers.handleCopy()
86+
expect(result).toEqual({ isCopied: true, isHovered: false })
87+
})
88+
})
89+
})
90+
91+
describe('CopyButton - exported constants', () => {
92+
test('COPIED_RESET_DELAY_MS is 2000ms', () => {
93+
expect(COPIED_RESET_DELAY_MS).toBe(2000)
94+
})
95+
96+
test('icon constants are defined', () => {
97+
expect(COPY_ICON_COLLAPSED).toBe('⎘')
98+
expect(COPY_ICON_EXPANDED).toBe('[⎘ copy]')
99+
expect(COPY_ICON_COPIED).toBe('[✔ copied]')
100+
})
101+
})
102+
103+
describe('CopyButton - copied state reset timing', () => {
104+
let originalSetTimeout: typeof setTimeout
105+
let originalClearTimeout: typeof clearTimeout
106+
let timers: { id: number; ms: number; fn: Function; active: boolean }[]
107+
let nextId: number
108+
109+
const runTimers = () => {
110+
for (const t of timers) {
111+
if (t.active) t.fn()
112+
}
113+
timers = []
114+
}
115+
116+
beforeEach(() => {
117+
timers = []
118+
nextId = 1
119+
originalSetTimeout = globalThis.setTimeout
120+
originalClearTimeout = globalThis.clearTimeout
121+
122+
globalThis.setTimeout = ((fn: Function, ms?: number) => {
123+
const id = nextId++
124+
timers.push({ id, ms: Number(ms ?? 0), fn, active: true })
125+
return id as any
126+
}) as any
127+
128+
globalThis.clearTimeout = ((id?: any) => {
129+
const rec = timers.find((t) => t.id === id)
130+
if (rec) rec.active = false
131+
}) as any
132+
})
133+
134+
afterEach(() => {
135+
globalThis.setTimeout = originalSetTimeout
136+
globalThis.clearTimeout = originalClearTimeout
137+
})
138+
139+
test('uses the exported COPIED_RESET_DELAY_MS constant (2000ms)', () => {
140+
let isCopied = false
141+
142+
// Simulate handleCopy using the exported constant
143+
const handleCopy = () => {
144+
const newState = copyButtonHandlers.handleCopy()
145+
isCopied = newState.isCopied
146+
setTimeout(() => {
147+
isCopied = false
148+
}, COPIED_RESET_DELAY_MS)
149+
}
150+
151+
handleCopy()
152+
expect(isCopied).toBe(true)
153+
expect(timers.length).toBe(1)
154+
expect(timers[0].ms).toBe(COPIED_RESET_DELAY_MS)
155+
156+
runTimers()
157+
expect(isCopied).toBe(false)
158+
})
159+
160+
test('multiple rapid clicks only create one active timer', () => {
161+
let isCopied = false
162+
let currentTimerId: number | null = null
163+
164+
const handleCopy = () => {
165+
if (currentTimerId !== null) {
166+
clearTimeout(currentTimerId)
167+
}
168+
const newState = copyButtonHandlers.handleCopy()
169+
isCopied = newState.isCopied
170+
currentTimerId = setTimeout(() => {
171+
isCopied = false
172+
}, COPIED_RESET_DELAY_MS) as unknown as number
173+
}
174+
175+
handleCopy()
176+
handleCopy()
177+
handleCopy()
178+
179+
const activeTimers = timers.filter((t) => t.active)
180+
expect(activeTimers.length).toBe(1)
181+
})
182+
})
183+

cli/src/__tests__/unit/copy-icon-button.test.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

0 commit comments

Comments
 (0)