Skip to content

Commit 5360a88

Browse files
its-mashMohammod Al Amin Ashik
andauthored
Fixes event subscription not working (#4)
* fixes event subscription not working * unit tests * adds changelog --------- Co-authored-by: Mohammod Al Amin Ashik <alamin.ashik@sitecore.com>
1 parent 8ac41c9 commit 5360a88

File tree

4 files changed

+309
-54
lines changed

4 files changed

+309
-54
lines changed

.changeset/olive-llamas-spend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sitecore-marketplace-sdk/client': patch
3+
---
4+
5+
Fixes event subscription not working

packages/client/src/__tests__/client.test.ts

Lines changed: 256 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import { ClientSDK } from '../client';
3-
import {CoreSDK, GenericRequestData} from '@sitecore-marketplace-sdk/core';
3+
import { CoreSDK, GenericRequestData } from '@sitecore-marketplace-sdk/core';
44
import { StateManager } from '../state';
55
import { logger } from '../logger';
66

@@ -15,7 +15,7 @@ describe('ClientSDK', () => {
1515
origin: 'https://host.app',
1616
target: window.parent,
1717
targetOrigin: 'https://target.app',
18-
selfOrigin: 'https://self.app'
18+
selfOrigin: 'https://self.app',
1919
};
2020

2121
const mockData = {
@@ -59,7 +59,7 @@ describe('ClientSDK', () => {
5959
target: window.parent,
6060
targetOrigin: 'https://target.app',
6161
selfOrigin: 'https://self.app',
62-
modules : [{ namespace: 'testModule', invokeOperation: vi.fn() }]
62+
modules: [{ namespace: 'testModule', invokeOperation: vi.fn() }],
6363
};
6464

6565
client = await ClientSDK.init(clientConfig);
@@ -111,7 +111,7 @@ describe('ClientSDK', () => {
111111
it('should call registerHandlers if config.events is defined', async () => {
112112
const clientConfig = {
113113
...config,
114-
events: { onPageContextUpdate: vi.fn() }
114+
events: { onPageContextUpdate: vi.fn() },
115115
};
116116
const registerHandlersSpy = vi.spyOn(ClientSDK.prototype as any, 'registerHandlers');
117117
client = await ClientSDK.init(clientConfig);
@@ -130,7 +130,7 @@ describe('ClientSDK', () => {
130130
const payload = { some: 'data' };
131131
const clientConfig = {
132132
...config,
133-
events: { onPageContextUpdate: vi.fn() }
133+
events: { onPageContextUpdate: vi.fn() },
134134
};
135135
(CoreSDK as any).mockImplementation(() => ({
136136
connect: vi.fn(),
@@ -171,6 +171,36 @@ describe('ClientSDK', () => {
171171
expect(logger.debug).toHaveBeenCalledWith('onPageContextUpdate event listener is not set.');
172172
});
173173

174+
it('should use key as hashedKey and call handleSubscription when subscribe is true', async () => {
175+
client = await ClientSDK.init(config);
176+
const key = 'host.state';
177+
const options = { subscribe: true };
178+
const handleSubscriptionSpy = vi
179+
.spyOn(client as any, 'handleSubscription')
180+
.mockReturnValue(() => {});
181+
const generateKeyWithHashSpy = vi.spyOn(client as any, 'generateKeyWithHash');
182+
183+
await client.query(key, options);
184+
185+
expect(handleSubscriptionSpy).toHaveBeenCalledWith(key, undefined, undefined);
186+
expect(generateKeyWithHashSpy).not.toHaveBeenCalled();
187+
});
188+
189+
it('should generate hashedKey and NOT call handleSubscription when subscribe is false', async () => {
190+
client = await ClientSDK.init(config);
191+
const key = 'host.state';
192+
const options = { subscribe: false };
193+
const handleSubscriptionSpy = vi.spyOn(client as any, 'handleSubscription');
194+
const generateKeyWithHashSpy = vi
195+
.spyOn(client as any, 'generateKeyWithHash')
196+
.mockResolvedValue('hashed-key');
197+
198+
await client.query(key, options);
199+
200+
expect(generateKeyWithHashSpy).toHaveBeenCalledWith(key, options);
201+
expect(handleSubscriptionSpy).not.toHaveBeenCalled();
202+
});
203+
174204
it('should generate unique keys for different query options', async () => {
175205
const key = 'host.user';
176206
const options1 = { subscribe: true };
@@ -183,8 +213,8 @@ describe('ClientSDK', () => {
183213
});
184214

185215
it('should update query state with unique keys', async () => {
186-
const key = 'host.user';
187-
const options = { subscribe: true };
216+
const key = 'xmc.request';
217+
const options = { subscribe: false };
188218
const hashedKey = await client['generateKeyWithHash'](key, options);
189219

190220
await client.query(key, options);
@@ -194,16 +224,19 @@ describe('ClientSDK', () => {
194224
});
195225
});
196226

197-
it('should handle query success with unique keys', async () => {
227+
it('should handle query success with unique keys when subscribe is false', async () => {
198228
client = await ClientSDK.init(config);
199-
const key = 'host.request';
200-
const options = { subscribe: true, params: {
229+
const key = 'xmc.request';
230+
const options = {
231+
subscribe: false,
232+
params: {
201233
path: '/api/resource',
202234
method: 'GET',
203235
headers: { 'Content-Type': 'application/json' },
204236
query: { id: '123' },
205-
requiresAuth: true
206-
} };
237+
requiresAuth: true,
238+
},
239+
};
207240
const hashedKey = await client['generateKeyWithHash'](key, options);
208241
const responseData = { id: 1, name: 'User' };
209242

@@ -223,6 +256,38 @@ describe('ClientSDK', () => {
223256
expect(result.data).toEqual(responseData);
224257
});
225258

259+
it('should handle query success with event key when subscribe is true', async () => {
260+
client = await ClientSDK.init(config);
261+
const key = 'host.request';
262+
const options = {
263+
subscribe: true,
264+
params: {
265+
path: '/api/resource',
266+
method: 'GET',
267+
headers: { 'Content-Type': 'application/json' },
268+
query: { id: '123' },
269+
requiresAuth: true,
270+
},
271+
};
272+
const hashedKey = key;
273+
const responseData = { id: 1, name: 'User' };
274+
275+
(CoreSDK.prototype.request as vi.Mock).mockResolvedValue(responseData);
276+
(StateManager.prototype.getQueryState as vi.Mock).mockReturnValue({
277+
status: 'success',
278+
data: responseData,
279+
});
280+
281+
const result = await client.query(key, options);
282+
283+
expect(StateManager.prototype.updateQueryState).toHaveBeenCalledWith(hashedKey, {
284+
status: 'success',
285+
data: responseData,
286+
});
287+
expect(StateManager.prototype.getQueryState).toHaveBeenCalledWith(hashedKey);
288+
expect(result.data).toEqual(responseData);
289+
});
290+
226291
it('should handle query error with correct state update and logging', async () => {
227292
const key = 'host.request';
228293
const options = { subscribe: true };
@@ -251,8 +316,8 @@ describe('ClientSDK', () => {
251316

252317
it('should handle query success', async () => {
253318
StateManager.prototype.getQueryState = vi
254-
.fn()
255-
.mockReturnValue({ status: 'idle', subscriptionCount: 0, data: mockData });
319+
.fn()
320+
.mockReturnValue({ status: 'idle', subscriptionCount: 0, data: mockData });
256321
CoreSDK.prototype.request = vi.fn().mockReturnValue(mockData);
257322
client = await ClientSDK.init(config);
258323

@@ -274,7 +339,7 @@ describe('ClientSDK', () => {
274339
});
275340

276341
it('should handle mutation success', async () => {
277-
const mutationKey='host.request';
342+
const mutationKey = 'host.request';
278343
const params: GenericRequestData = { path: '/some-path', requiresAuth: true };
279344
const timeout = 5000;
280345
CoreSDK.prototype.request = vi.fn().mockReturnValue(mockData);
@@ -284,9 +349,9 @@ describe('ClientSDK', () => {
284349

285350
expect(result).toEqual(mockData);
286351
expect(logger.debug).toHaveBeenCalledWith(
287-
`Mutation (${mutationKey}) initiated with params:`,
288-
params,
289-
`timeoutMs: ${timeout}`,
352+
`Mutation (${mutationKey}) initiated with params:`,
353+
params,
354+
`timeoutMs: ${timeout}`,
290355
);
291356
expect(logger.info).toHaveBeenCalledWith(`Mutation (${mutationKey}) success:`, mockData);
292357
});
@@ -305,7 +370,7 @@ describe('ClientSDK', () => {
305370
it('should clean up all queries on destroy', () => {
306371
StateManager.prototype.getQueryKeys = vi.fn().mockReturnValue(['query1', 'query2']);
307372
const clientSDK = new ClientSDK(config);
308-
const unsubscribeSpy = vi.spyOn(clientSDK as any, 'unsubscribe');
373+
const unsubscribeSpy = vi.spyOn(clientSDK as any, '_unsubscribe');
309374

310375
clientSDK.destroy();
311376

@@ -355,7 +420,9 @@ describe('ClientSDK', () => {
355420
});
356421

357422
// Test with missing URL
358-
await expect(client.navigateToExternalUrl('')).rejects.toThrow('URL is required for navigateToExternalUrl');
423+
await expect(client.navigateToExternalUrl('')).rejects.toThrow(
424+
'URL is required for navigateToExternalUrl',
425+
);
359426
});
360427

361428
it('should unsubscribe correctly', async () => {
@@ -371,9 +438,176 @@ describe('ClientSDK', () => {
371438
StateManager.prototype.removeQuery = vi.fn();
372439

373440
client = await ClientSDK.init(config);
374-
client['unsubscribe'](hashedKey);
441+
client['_unsubscribe'](hashedKey);
375442

376443
expect(StateManager.prototype.decrementSubscriptionCount).toHaveBeenCalledWith(hashedKey);
377444
expect(StateManager.prototype.removeQuery).toHaveBeenCalledWith(hashedKey);
378445
});
379-
});
446+
447+
it('should handle subscription: call onSuccess and onError, manage subscription count', () => {
448+
const clientSDK = new ClientSDK(config);
449+
const hashedKey = 'test.key';
450+
const mockOnSuccess = vi.fn();
451+
const mockOnError = vi.fn();
452+
// Mock stateManager methods
453+
const subscribeMock = vi.fn((_, cb) => {
454+
// Simulate state change with data
455+
cb({ data: 'test-data', error: undefined });
456+
// Simulate state change with error
457+
cb({ data: undefined, error: new Error('test-error') });
458+
return vi.fn(); // unsubscribe function
459+
});
460+
const incrementMock = vi.fn();
461+
const getCountMock = vi.fn().mockReturnValue(1);
462+
const updateQueryStateMock = vi.fn();
463+
// Mock coreSdk.on to return an unsubscribe function
464+
clientSDK['coreSdk'] = {
465+
on: vi.fn().mockReturnValue(vi.fn()),
466+
} as any;
467+
clientSDK['stateManager'] = {
468+
subscribe: subscribeMock,
469+
incrementSubscriptionCount: incrementMock,
470+
getSubscriptionCount: getCountMock,
471+
updateQueryState: updateQueryStateMock,
472+
} as any;
473+
// Call handleSubscription
474+
const unsubscribe = (clientSDK as any).handleSubscription(
475+
hashedKey,
476+
mockOnSuccess,
477+
mockOnError,
478+
);
479+
// Should call subscribe and increment
480+
expect(subscribeMock).toHaveBeenCalledWith(hashedKey, expect.any(Function));
481+
expect(incrementMock).toHaveBeenCalledWith(hashedKey);
482+
expect(getCountMock).toHaveBeenCalledWith(hashedKey);
483+
// Should call onSuccess and onError
484+
expect(mockOnSuccess).toHaveBeenCalledWith('test-data');
485+
expect(mockOnError).toHaveBeenCalledWith(new Error('test-error'));
486+
// Should return an unsubscribe function
487+
expect(typeof unsubscribe).toBe('function');
488+
});
489+
490+
it('should not call coreSdk.on if subscription count > 1', () => {
491+
const clientSDK = new ClientSDK(config);
492+
const hashedKey = 'test.key';
493+
const mockOnSuccess = vi.fn();
494+
const mockOnError = vi.fn();
495+
// Mock stateManager methods
496+
const subscribeMock = vi.fn();
497+
const incrementMock = vi.fn();
498+
const getCountMock = vi.fn().mockReturnValue(2); // > 1
499+
const updateQueryStateMock = vi.fn();
500+
clientSDK['coreSdk'] = {
501+
on: vi.fn(),
502+
} as any;
503+
clientSDK['stateManager'] = {
504+
subscribe: subscribeMock,
505+
incrementSubscriptionCount: incrementMock,
506+
getSubscriptionCount: getCountMock,
507+
updateQueryState: updateQueryStateMock,
508+
} as any;
509+
(clientSDK as any).handleSubscription(hashedKey, mockOnSuccess, mockOnError);
510+
expect(clientSDK['coreSdk'].on).not.toHaveBeenCalled();
511+
});
512+
513+
it('should call only onSuccess if state has data, only onError if state has error, and neither if state is empty', () => {
514+
const clientSDK = new ClientSDK(config);
515+
const hashedKey = 'test.key';
516+
const mockOnSuccess = vi.fn();
517+
const mockOnError = vi.fn();
518+
let stateCallback: any;
519+
const subscribeMock = vi.fn((_, cb) => {
520+
stateCallback = cb;
521+
return vi.fn();
522+
});
523+
const incrementMock = vi.fn();
524+
const getCountMock = vi.fn().mockReturnValue(2);
525+
const updateQueryStateMock = vi.fn();
526+
clientSDK['coreSdk'] = { on: vi.fn() } as any;
527+
clientSDK['stateManager'] = {
528+
subscribe: subscribeMock,
529+
incrementSubscriptionCount: incrementMock,
530+
getSubscriptionCount: getCountMock,
531+
updateQueryState: updateQueryStateMock,
532+
} as any;
533+
(clientSDK as any).handleSubscription(hashedKey, mockOnSuccess, mockOnError);
534+
// Only data
535+
stateCallback({ data: 'data', error: undefined });
536+
expect(mockOnSuccess).toHaveBeenCalledWith('data');
537+
// Only error
538+
stateCallback({ data: undefined, error: new Error('err') });
539+
expect(mockOnError).toHaveBeenCalledWith(new Error('err'));
540+
// Neither
541+
stateCallback({ data: undefined, error: undefined });
542+
// Should not throw
543+
});
544+
545+
it('should not throw if onSuccess/onError are not provided', () => {
546+
const clientSDK = new ClientSDK(config);
547+
const hashedKey = 'test.key';
548+
let stateCallback: any;
549+
const subscribeMock = vi.fn((_, cb) => {
550+
stateCallback = cb;
551+
return vi.fn();
552+
});
553+
const incrementMock = vi.fn();
554+
const getCountMock = vi.fn().mockReturnValue(2);
555+
const updateQueryStateMock = vi.fn();
556+
clientSDK['coreSdk'] = { on: vi.fn() } as any;
557+
clientSDK['stateManager'] = {
558+
subscribe: subscribeMock,
559+
incrementSubscriptionCount: incrementMock,
560+
getSubscriptionCount: getCountMock,
561+
updateQueryState: updateQueryStateMock,
562+
} as any;
563+
(clientSDK as any).handleSubscription(hashedKey);
564+
expect(() => stateCallback({ data: 'data', error: undefined })).not.toThrow();
565+
expect(() => stateCallback({ data: undefined, error: new Error('err') })).not.toThrow();
566+
});
567+
568+
it('should call both stateChangeUnsubscribe and _unsubscribe on returned unsubscribe', () => {
569+
const clientSDK = new ClientSDK(config);
570+
const hashedKey = 'test.key';
571+
const stateChangeUnsubscribe = vi.fn();
572+
const subscribeMock = vi.fn(() => stateChangeUnsubscribe);
573+
const incrementMock = vi.fn();
574+
const getCountMock = vi.fn().mockReturnValue(2);
575+
const updateQueryStateMock = vi.fn();
576+
const _unsubscribeMock = vi.spyOn(clientSDK as any, '_unsubscribe');
577+
clientSDK['coreSdk'] = { on: vi.fn() } as any;
578+
clientSDK['stateManager'] = {
579+
subscribe: subscribeMock,
580+
incrementSubscriptionCount: incrementMock,
581+
getSubscriptionCount: getCountMock,
582+
updateQueryState: updateQueryStateMock,
583+
getQueryState: vi.fn().mockReturnValue({}),
584+
decrementSubscriptionCount: vi.fn(),
585+
} as any;
586+
const unsub = (clientSDK as any).handleSubscription(hashedKey);
587+
unsub();
588+
expect(stateChangeUnsubscribe).toHaveBeenCalled();
589+
expect(_unsubscribeMock).toHaveBeenCalledWith(hashedKey);
590+
});
591+
592+
it('should store and call coreSdk.on unsubscribe when subscription count is 1', () => {
593+
const clientSDK = new ClientSDK(config);
594+
const hashedKey = 'test.key';
595+
const subscribeMock = vi.fn(() => vi.fn());
596+
const incrementMock = vi.fn();
597+
const getCountMock = vi.fn().mockReturnValue(1);
598+
const updateQueryStateMock = vi.fn();
599+
const coreUnsubscribe = vi.fn();
600+
clientSDK['coreSdk'] = {
601+
on: vi.fn().mockReturnValue(coreUnsubscribe),
602+
} as any;
603+
clientSDK['stateManager'] = {
604+
subscribe: subscribeMock,
605+
incrementSubscriptionCount: incrementMock,
606+
getSubscriptionCount: getCountMock,
607+
updateQueryState: updateQueryStateMock,
608+
} as any;
609+
(clientSDK as any).handleSubscription(hashedKey);
610+
expect(clientSDK['coreSdk'].on).toHaveBeenCalledWith(hashedKey, expect.any(Function));
611+
expect(updateQueryStateMock).toHaveBeenCalledWith(hashedKey, { unsubscribe: coreUnsubscribe });
612+
});
613+
});

0 commit comments

Comments
 (0)