Skip to content

Commit d4a5376

Browse files
authored
Merge pull request #6075 from Shopify/fix-memory-lead-event-listener
fix MaxListenersExceededWarning
2 parents 795168f + f284c9f commit d4a5376

File tree

2 files changed

+110
-3
lines changed

2 files changed

+110
-3
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {TextAnimation} from './TextAnimation.js'
2+
import {render} from '../../testing/ui.js'
3+
import {Stdout} from '../../ui.js'
4+
import React from 'react'
5+
import {beforeEach, describe, expect, test, vi} from 'vitest'
6+
import {useStdout} from 'ink'
7+
8+
vi.mock('ink', async () => {
9+
const original: any = await vi.importActual('ink')
10+
return {
11+
...original,
12+
useStdout: vi.fn(),
13+
}
14+
})
15+
16+
describe('TextAnimation', () => {
17+
let stdout: Stdout
18+
let onSpy: any
19+
let offSpy: any
20+
21+
beforeEach(() => {
22+
stdout = new Stdout({
23+
columns: 80,
24+
rows: 80,
25+
})
26+
27+
onSpy = vi.spyOn(stdout, 'on')
28+
offSpy = vi.spyOn(stdout, 'off')
29+
30+
vi.mocked(useStdout).mockReturnValue({
31+
stdout: stdout as any,
32+
write: () => {},
33+
})
34+
})
35+
36+
test('removes resize listener on unmount', async () => {
37+
const renderInstance = render(<TextAnimation text="Loading..." />)
38+
39+
expect(onSpy).toHaveBeenCalledWith('resize', expect.any(Function))
40+
expect(onSpy).toHaveBeenCalledTimes(1)
41+
42+
const resizeHandler = onSpy.mock.calls[0]![1]
43+
44+
renderInstance.unmount()
45+
46+
expect(offSpy).toHaveBeenCalledWith('resize', resizeHandler)
47+
expect(offSpy).toHaveBeenCalledTimes(1)
48+
})
49+
50+
test('renders animated text', async () => {
51+
const renderInstance = render(<TextAnimation text="Loading..." />)
52+
53+
expect(renderInstance.lastFrame()).toBeDefined()
54+
// The text is rendered with ANSI color codes, so we need to check for the content
55+
// without the color codes. The actual output contains the text but with color formatting.
56+
const frame = renderInstance.lastFrame() ?? ''
57+
expect(frame).toBeTruthy()
58+
expect(frame.length).toBeGreaterThan(0)
59+
60+
renderInstance.unmount()
61+
})
62+
63+
test('updates width when stdout resizes', async () => {
64+
const renderInstance = render(<TextAnimation text="Loading..." />)
65+
66+
expect(onSpy).toHaveBeenCalledWith('resize', expect.any(Function))
67+
const resizeHandler = onSpy.mock.calls[0]![1] as () => void
68+
69+
stdout.columns = 120
70+
resizeHandler()
71+
72+
await new Promise((resolve) => setTimeout(resolve, 50))
73+
74+
renderInstance.unmount()
75+
})
76+
77+
test('respects maxWidth prop when provided', async () => {
78+
const renderInstance = render(<TextAnimation text="Loading..." maxWidth={20} />)
79+
80+
expect(renderInstance.lastFrame()).toBeDefined()
81+
82+
renderInstance.unmount()
83+
})
84+
85+
test('cleans up animation timeout on unmount', async () => {
86+
vi.useFakeTimers()
87+
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
88+
89+
const renderInstance = render(<TextAnimation text="Loading..." />)
90+
91+
vi.advanceTimersByTime(50)
92+
93+
renderInstance.unmount()
94+
95+
expect(clearTimeoutSpy).toHaveBeenCalled()
96+
97+
vi.useRealTimers()
98+
})
99+
})

packages/cli-kit/src/private/node/ui/components/TextAnimation.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,17 @@ const TextAnimation = memo(({text, maxWidth}: TextAnimationProps): JSX.Element =
3636
const {stdout} = useStdout()
3737
const [width, setWidth] = useState(maxWidth ?? Math.floor(stdout.columns * 0.66))
3838

39-
stdout.on('resize', () => {
40-
setWidth(Math.floor(stdout.columns * 0.66))
41-
})
39+
useLayoutEffect(() => {
40+
const handleResize = () => {
41+
setWidth(Math.floor(stdout.columns * 0.66))
42+
}
43+
44+
stdout.on('resize', handleResize)
45+
46+
return () => {
47+
stdout.off('resize', handleResize)
48+
}
49+
}, [stdout])
4250

4351
const renderAnimation = useCallback(() => {
4452
const newFrame = frame.current + 1

0 commit comments

Comments
 (0)