Skip to content

Commit 91571c2

Browse files
committed
refactor + testing
1 parent 8775e4f commit 91571c2

File tree

8 files changed

+929
-289
lines changed

8 files changed

+929
-289
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { FeedbackRecordingManager, generateFeedbackRecording } from '../../extensions/feedback-recording'
2+
import * as FeedbackUI from '../../extensions/feedback-recording/components/FeedbackRecordingUI'
3+
import { PostHog } from '../../posthog-core'
4+
import { assignableWindow } from '../../utils/globals'
5+
import { createPosthogInstance } from '../helpers/posthog-instance'
6+
import { uuidv7 } from '../../uuidv7'
7+
import { AudioRecorder } from '../../extensions/feedback-recording/audio-recorder'
8+
import '@testing-library/jest-dom'
9+
10+
jest.mock('../../extensions/feedback-recording/components/FeedbackRecordingUI')
11+
12+
describe('FeedbackRecordingManager', () => {
13+
let instance: PostHog
14+
let manager: FeedbackRecordingManager
15+
let audioRecorderMock: jest.Mocked<AudioRecorder>
16+
let loadScriptMock: jest.Mock
17+
18+
beforeEach(async () => {
19+
// mock the renderFeedbackRecordingUI function
20+
21+
loadScriptMock = jest.fn()
22+
loadScriptMock.mockImplementation((_ph, _path, callback) => {
23+
assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
24+
assignableWindow.__PosthogExtensions__.generateFeedbackRecording = generateFeedbackRecording
25+
callback()
26+
})
27+
28+
assignableWindow.__PosthogExtensions__ = {
29+
loadExternalDependency: loadScriptMock,
30+
}
31+
32+
instance = await createPosthogInstance(uuidv7(), {
33+
api_host: 'https://test.com',
34+
token: 'test-token',
35+
})
36+
37+
// Create a properly mocked AudioRecorder
38+
audioRecorderMock = {
39+
startRecording: jest.fn().mockResolvedValue(undefined),
40+
stopRecording: jest.fn().mockResolvedValue(null),
41+
cancelRecording: jest.fn().mockResolvedValue(undefined),
42+
isRecording: jest.fn().mockReturnValue(false),
43+
isSupported: jest.fn().mockReturnValue(true),
44+
getSupportedMimeTypes: jest.fn().mockReturnValue(['audio/webm']),
45+
} as unknown as jest.Mocked<AudioRecorder>
46+
47+
manager = new FeedbackRecordingManager(instance, audioRecorderMock)
48+
49+
// Mock instance methods
50+
jest.spyOn(instance, 'capture').mockImplementation(jest.fn())
51+
jest.spyOn(instance, 'startSessionRecording').mockImplementation(jest.fn())
52+
jest.spyOn(instance, 'get_session_id').mockReturnValue('mock-session-id')
53+
})
54+
55+
it('should initialize with PostHog instance', () => {
56+
expect(manager).toBeInstanceOf(FeedbackRecordingManager)
57+
expect(manager.getCurrentFeedbackRecordingId()).toBeNull()
58+
expect(manager.isFeedbackRecordingActive()).toBe(false)
59+
})
60+
61+
describe('renderFeedbackRecordingUI', () => {
62+
it('should call renderFeedbackRecordingUI with correct props', () => {
63+
//TODO: this should probably test the call made to Preact
64+
})
65+
})
66+
67+
describe('launchFeedbackRecordingUI', () => {
68+
beforeEach(() => {
69+
// reset mocks
70+
jest.clearAllMocks()
71+
})
72+
73+
it('should handle loading state correctly', async () => {
74+
expect(assignableWindow.__PosthogExtensions__?.generateFeedbackRecording).toBeUndefined()
75+
76+
await manager.launchFeedbackRecordingUI(jest.fn())
77+
78+
expect(assignableWindow.__PosthogExtensions__?.generateFeedbackRecording).toBeDefined()
79+
expect(loadScriptMock).toHaveBeenCalledTimes(1)
80+
expect(FeedbackUI.renderFeedbackRecordingUI).toHaveBeenCalled()
81+
82+
// if called again, should not load again
83+
await manager.launchFeedbackRecordingUI(jest.fn())
84+
expect(loadScriptMock).toHaveBeenCalledTimes(1)
85+
expect(FeedbackUI.renderFeedbackRecordingUI).toHaveBeenCalled()
86+
})
87+
88+
it('should successfully launch UI when no recording is active', async () => {
89+
expect(manager.isFeedbackRecordingActive()).toBe(false)
90+
91+
const callback = jest.fn()
92+
await manager.launchFeedbackRecordingUI(callback)
93+
94+
expect(FeedbackUI.renderFeedbackRecordingUI).toHaveBeenCalledTimes(1)
95+
96+
expect(FeedbackUI.renderFeedbackRecordingUI).toHaveBeenCalledWith(
97+
expect.objectContaining({
98+
posthogInstance: instance,
99+
handleStartRecording: expect.any(Function),
100+
onRecordingEnded: expect.any(Function),
101+
})
102+
)
103+
104+
// the recording is launched via the UI
105+
expect(manager.isFeedbackRecordingActive()).toBe(false)
106+
expect(callback).not.toHaveBeenCalled()
107+
})
108+
109+
it('should not launch UI when loading fails', async () => {
110+
assignableWindow.__PosthogExtensions__ = {
111+
loadExternalDependency: jest.fn((_ph, _name, cb) => {
112+
cb('Load failed')
113+
}),
114+
}
115+
116+
const callback = jest.fn()
117+
await manager.launchFeedbackRecordingUI(callback)
118+
119+
// UI should not be rendered when loading fails
120+
expect(FeedbackUI.renderFeedbackRecordingUI).not.toHaveBeenCalled()
121+
})
122+
123+
it('should handle concurrent loading attempts', async () => {
124+
const promise1 = manager.launchFeedbackRecordingUI(jest.fn())
125+
const promise2 = manager.launchFeedbackRecordingUI(jest.fn())
126+
127+
await Promise.all([promise1, promise2])
128+
129+
expect(assignableWindow.__PosthogExtensions__?.loadExternalDependency).toHaveBeenCalledTimes(1)
130+
})
131+
132+
describe('returning a callback which starts a recording', () => {
133+
it('calls the correct audio recording methods', async () => {
134+
await manager.launchFeedbackRecordingUI()
135+
136+
const handleStartRecording = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].handleStartRecording
137+
138+
await handleStartRecording()
139+
140+
expect(audioRecorderMock.startRecording).toHaveBeenCalledTimes(1)
141+
expect(manager.isFeedbackRecordingActive()).toBe(true)
142+
expect(manager.getCurrentFeedbackRecordingId()).not.toBeNull()
143+
})
144+
145+
it('captures an event when recording starts', async () => {
146+
await manager.launchFeedbackRecordingUI()
147+
148+
const handleStartRecording = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].handleStartRecording
149+
150+
const feedbackId = await handleStartRecording()
151+
152+
expect(instance.capture).toHaveBeenCalledWith('$user_feedback_recording_started', {
153+
$feedback_recording_id: feedbackId,
154+
})
155+
})
156+
})
157+
158+
describe('onRecordingEnded can be used to stop and upload the recording', () => {
159+
it('returns a callback which stops a recording', async () => {
160+
const onRecordingEnded = jest.fn()
161+
162+
await manager.launchFeedbackRecordingUI(onRecordingEnded)
163+
164+
const handleStartRecording = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].handleStartRecording
165+
166+
const feedbackId = await handleStartRecording()
167+
168+
const stopCallback = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].onRecordingEnded
169+
170+
await stopCallback(feedbackId)
171+
172+
expect(audioRecorderMock.stopRecording).toHaveBeenCalledTimes(1)
173+
expect(onRecordingEnded).toHaveBeenCalledTimes(1)
174+
expect(manager.isFeedbackRecordingActive()).toBe(false)
175+
expect(manager.getCurrentFeedbackRecordingId()).toBeNull()
176+
})
177+
178+
it('captures an event when recording stops', async () => {
179+
const onRecordingEnded = jest.fn()
180+
181+
await manager.launchFeedbackRecordingUI(onRecordingEnded)
182+
183+
const handleStartRecording = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].handleStartRecording
184+
185+
const feedbackId = await handleStartRecording()
186+
187+
const stopCallback = FeedbackUI.renderFeedbackRecordingUI.mock.lastCall[0].onRecordingEnded
188+
189+
await stopCallback(feedbackId)
190+
191+
expect(instance.capture).toHaveBeenCalledWith('$user_feedback_recording_stopped', {
192+
$feedback_recording_id: feedbackId,
193+
})
194+
})
195+
})
196+
})
197+
198+
describe('generateFeedbackRecording', () => {
199+
it('should create a new FeedbackRecordingManager instance', () => {
200+
const result = generateFeedbackRecording(instance)
201+
202+
expect(result).toBeInstanceOf(FeedbackRecordingManager)
203+
})
204+
})
205+
})

0 commit comments

Comments
 (0)