@@ -35,6 +35,7 @@ jest.mock('@imtbl/audience-core', () => ({
3535 } ) ,
3636 generateId : jest . fn ( ) . mockReturnValue ( 'msg-uuid' ) ,
3737 getTimestamp : jest . fn ( ) . mockReturnValue ( '2026-04-07T00:00:00.000Z' ) ,
38+ isBrowser : jest . fn ( ) . mockReturnValue ( true ) ,
3839 getCookie : jest . fn ( ) ,
3940 setCookie : jest . fn ( ) ,
4041} ) ) ;
@@ -48,60 +49,99 @@ jest.mock('./attribution', () => ({
4849} ) ) ;
4950
5051jest . mock ( './session' , ( ) => ( {
52+ getOrCreateSession : jest . fn ( ) . mockReturnValue ( { sessionId : 'session-abc' , isNew : true } ) ,
5153 getOrCreateSessionId : jest . fn ( ) . mockReturnValue ( 'session-abc' ) ,
5254} ) ) ;
5355
5456// Mock fetch globally
5557global . fetch = jest . fn ( ) . mockResolvedValue ( { ok : true } ) ;
5658
59+ // Access the mock to change return values per test
60+ const mockGetOrCreateSession = jest . requireMock ( './session' ) . getOrCreateSession ;
61+
62+ let activePixel : Pixel | null = null ;
63+
5764beforeEach ( ( ) => {
5865 jest . clearAllMocks ( ) ;
66+ mockGetOrCreateSession . mockReturnValue ( { sessionId : 'session-abc' , isNew : true } ) ;
67+ } ) ;
68+
69+ afterEach ( ( ) => {
70+ // Clean up any active pixel to remove event listeners
71+ if ( activePixel ) {
72+ activePixel . destroy ( ) ;
73+ activePixel = null ;
74+ }
5975} ) ;
6076
6177describe ( 'Pixel' , ( ) => {
6278 describe ( 'init' , ( ) => {
63- it ( 'creates queue, starts it, and fires a page view when consent is anonymous' , ( ) => {
79+ it ( 'creates queue, starts it, and fires page view + session_start when consent is anonymous' , ( ) => {
6480 const pixel = new Pixel ( ) ;
81+ activePixel = pixel ;
6582 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
6683
6784 expect ( mockStart ) . toHaveBeenCalled ( ) ;
68- expect ( mockEnqueue ) . toHaveBeenCalledWith (
69- expect . objectContaining ( {
70- type : 'page' ,
71- surface : 'pixel' ,
72- anonymousId : 'anon-123' ,
73- properties : expect . objectContaining ( {
74- utm_source : 'google' ,
75- sessionId : 'session-abc' ,
76- } ) ,
77- } ) ,
85+
86+ // Should fire session_start (new session) then page view
87+ const calls = mockEnqueue . mock . calls . map ( ( c : unknown [ ] ) => ( c [ 0 ] as Record < string , unknown > ) ) ;
88+ const pageCall = calls . find ( ( c ) => c . type === 'page' ) ;
89+ const sessionStartCall = calls . find (
90+ ( c ) => c . type === 'track' && c . eventName === 'session_start' ,
7891 ) ;
92+
93+ expect ( pageCall ) . toBeDefined ( ) ;
94+ expect ( pageCall ! . surface ) . toBe ( 'pixel' ) ;
95+ expect ( pageCall ! . anonymousId ) . toBe ( 'anon-123' ) ;
96+ expect ( ( pageCall ! . properties as Record < string , unknown > ) . utm_source ) . toBe ( 'google' ) ;
97+
98+ expect ( sessionStartCall ) . toBeDefined ( ) ;
99+ expect ( ( sessionStartCall ! . properties as Record < string , unknown > ) . sessionId ) . toBe ( 'session-abc' ) ;
79100 } ) ;
80101
81- it ( 'does not fire page view when consent is none' , ( ) => {
102+ it ( 'does not fire page view or session_start when consent is none' , ( ) => {
82103 const pixel = new Pixel ( ) ;
104+ activePixel = pixel ;
83105 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'none' } ) ;
84106
85107 expect ( mockStart ) . toHaveBeenCalled ( ) ;
86108 expect ( mockEnqueue ) . not . toHaveBeenCalled ( ) ;
87109 } ) ;
88110
111+ it ( 'does not fire session_start for existing sessions' , ( ) => {
112+ mockGetOrCreateSession . mockReturnValue ( { sessionId : 'session-abc' , isNew : false } ) ;
113+
114+ const pixel = new Pixel ( ) ;
115+ activePixel = pixel ;
116+ pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
117+
118+ const calls = mockEnqueue . mock . calls . map ( ( c : unknown [ ] ) => ( c [ 0 ] as Record < string , unknown > ) ) ;
119+ const sessionStartCall = calls . find (
120+ ( c ) => c . type === 'track' && c . eventName === 'session_start' ,
121+ ) ;
122+
123+ expect ( sessionStartCall ) . toBeUndefined ( ) ;
124+ // Page view should still fire
125+ expect ( calls . find ( ( c ) => c . type === 'page' ) ) . toBeDefined ( ) ;
126+ } ) ;
127+
89128 it ( 'only initializes once' , ( ) => {
90129 const pixel = new Pixel ( ) ;
130+ activePixel = pixel ;
91131 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
92132 pixel . init ( { key : 'pk_other' , environment : 'dev' , consent : 'anonymous' } ) ;
93133
94- // Start called only once
95134 expect ( mockStart ) . toHaveBeenCalledTimes ( 1 ) ;
96135 } ) ;
97136 } ) ;
98137
99138 describe ( 'page' , ( ) => {
100139 it ( 'enqueues a page message with attribution and session' , ( ) => {
140+ mockGetOrCreateSession . mockReturnValue ( { sessionId : 'session-abc' , isNew : false } ) ;
141+
101142 const pixel = new Pixel ( ) ;
143+ activePixel = pixel ;
102144 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
103-
104- // Clear the auto-fired page view
105145 mockEnqueue . mockClear ( ) ;
106146
107147 pixel . page ( { custom : 'prop' } ) ;
@@ -121,6 +161,7 @@ describe('Pixel', () => {
121161
122162 it ( 'does not enqueue when consent is none' , ( ) => {
123163 const pixel = new Pixel ( ) ;
164+ activePixel = pixel ;
124165 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'none' } ) ;
125166
126167 pixel . page ( ) ;
@@ -130,7 +171,10 @@ describe('Pixel', () => {
130171
131172 describe ( 'identify' , ( ) => {
132173 it ( 'enqueues identify message at full consent' , ( ) => {
174+ mockGetOrCreateSession . mockReturnValue ( { sessionId : 'session-abc' , isNew : false } ) ;
175+
133176 const pixel = new Pixel ( ) ;
177+ activePixel = pixel ;
134178 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'full' } ) ;
135179 mockEnqueue . mockClear ( ) ;
136180
@@ -151,23 +195,79 @@ describe('Pixel', () => {
151195
152196 it ( 'does not enqueue identify at anonymous consent' , ( ) => {
153197 const pixel = new Pixel ( ) ;
198+ activePixel = pixel ;
154199 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
155200
156201 pixel . identify ( 'user-1' ) ;
157- // Only the auto page view, no identify
158- expect ( mockEnqueue ) . toHaveBeenCalledTimes ( 1 ) ;
159- expect ( mockEnqueue ) . toHaveBeenCalledWith ( expect . objectContaining ( { type : 'page' } ) ) ;
202+ // Only the auto page view + session_start, no identify
203+ const calls = mockEnqueue . mock . calls . map ( ( c : unknown [ ] ) => ( c [ 0 ] as Record < string , unknown > ) ) ;
204+ expect ( calls . find ( ( c ) => c . type === 'identify' ) ) . toBeUndefined ( ) ;
205+ } ) ;
206+ } ) ;
207+
208+ describe ( 'session_end' , ( ) => {
209+ it ( 'fires session_end on pagehide when session is active' , ( ) => {
210+ const pixel = new Pixel ( ) ;
211+ activePixel = pixel ;
212+ pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
213+ mockEnqueue . mockClear ( ) ;
214+
215+ // Simulate pagehide
216+ window . dispatchEvent ( new Event ( 'pagehide' ) ) ;
217+
218+ expect ( mockEnqueue ) . toHaveBeenCalledWith (
219+ expect . objectContaining ( {
220+ type : 'track' ,
221+ eventName : 'session_end' ,
222+ properties : expect . objectContaining ( {
223+ sessionId : 'session-abc' ,
224+ } ) ,
225+ } ) ,
226+ ) ;
227+ } ) ;
228+
229+ it ( 'includes duration in session_end' , ( ) => {
230+ const dateNowSpy = jest . spyOn ( Date , 'now' ) . mockReturnValue ( 1000000 ) ;
231+
232+ const pixel = new Pixel ( ) ;
233+ activePixel = pixel ;
234+ pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
235+ mockEnqueue . mockClear ( ) ;
236+
237+ // Advance time by 15 seconds before triggering pagehide
238+ dateNowSpy . mockReturnValue ( 1015000 ) ;
239+ window . dispatchEvent ( new Event ( 'pagehide' ) ) ;
240+
241+ const sessionEndCall = mockEnqueue . mock . calls . find (
242+ ( c : unknown [ ] ) => ( c [ 0 ] as Record < string , unknown > ) . eventName === 'session_end' ,
243+ ) ;
244+ expect ( sessionEndCall ) . toBeDefined ( ) ;
245+ expect ( ( sessionEndCall ! [ 0 ] as Record < string , unknown > ) . properties ) . toEqual (
246+ expect . objectContaining ( { duration : 15 } ) ,
247+ ) ;
248+
249+ dateNowSpy . mockRestore ( ) ;
250+ } ) ;
251+
252+ it ( 'does not fire session_end when consent is none' , ( ) => {
253+ const pixel = new Pixel ( ) ;
254+ activePixel = pixel ;
255+ pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'none' } ) ;
256+
257+ window . dispatchEvent ( new Event ( 'pagehide' ) ) ;
258+ expect ( mockEnqueue ) . not . toHaveBeenCalled ( ) ;
160259 } ) ;
161260 } ) ;
162261
163262 describe ( 'setConsent' , ( ) => {
164263 it ( 'updates consent level' , ( ) => {
165264 const pixel = new Pixel ( ) ;
265+ activePixel = pixel ;
166266 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'none' } ) ;
167267
168268 pixel . setConsent ( 'anonymous' ) ;
169269
170- // After upgrading consent, page() should work
270+ mockGetOrCreateSession . mockReturnValue ( { sessionId : 'session-xyz' , isNew : false } ) ;
171271 pixel . page ( ) ;
172272 expect ( mockEnqueue ) . toHaveBeenCalledWith ( expect . objectContaining ( { type : 'page' } ) ) ;
173273 } ) ;
@@ -176,6 +276,7 @@ describe('Pixel', () => {
176276 describe ( 'destroy' , ( ) => {
177277 it ( 'destroys the queue and resets state' , ( ) => {
178278 const pixel = new Pixel ( ) ;
279+ activePixel = pixel ;
179280 pixel . init ( { key : 'pk_test' , environment : 'dev' , consent : 'anonymous' } ) ;
180281
181282 pixel . destroy ( ) ;
0 commit comments