Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/roktManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export interface IRoktKit {
setExtensionData<T>(extensionData: IRoktPartnerExtensionData<T>): void;
use: <T>(name: string) => Promise<T>;
onShoppableAdsReady(callback: () => void): void;
// Optional because the Rokt Kit ships on its own release cadence; a kit
// published before terminate() existed will not implement it.
terminate?: () => Promise<void>;
launcherOptions?: Dictionary<any>;
settings?: IRoktKitSettings;
integrationName?: string;
Expand Down Expand Up @@ -479,6 +482,40 @@ export default class RoktManager {
}
}

/**
* Tears down the current Rokt launcher and removes the placements it rendered.
*
* Intended for hosts that cannot simply destroy the container element —
* SPA route changes and confirmation pages, for example.
*
* Unlike the other proxied methods, a call made before the kit is ready is
* *not* queued. Replaying a teardown against a launcher created later would
* silently tear down placements the caller never asked to remove; with no
* launcher there is nothing to terminate, so this resolves as a no-op.
*
* @example
* await window.mParticle.Rokt.terminate();
*
* @returns {Promise<void>} Resolves once the launcher has torn down. Never rejects.
*/
public async terminate(): Promise<void> {
if (!this.isReady()) {
this.logger?.verbose('mParticle.Rokt.terminate called before the Rokt kit was ready. Nothing to terminate.');
return;
}

if (!isFunction(this.kit.terminate)) {
this.logger?.error('mParticle.Rokt.terminate is not supported by the attached Rokt Kit version.');
return;
}

try {
await this.kit.terminate();
} catch (error) {
this.logger?.error(`Failed to terminate the Rokt launcher: ${getErrorMessage(error)}`);
}
}

public onShoppableAdsReady(callback: () => void): void {
if (!this.kit || !this.isShoppableAdsLoaded) {
this.deferredCall<void>('onShoppableAdsReady', callback);
Expand Down
100 changes: 100 additions & 0 deletions test/jest/roktManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3007,6 +3007,106 @@ describe('RoktManager', () => {
});
});

describe('#terminate', () => {
const makeKit = (): IRoktKit => ({
launcher: {
selectPlacements: jest.fn(),
hashAttributes: jest.fn(),
use: jest.fn(),
},
filters: undefined,
filteredUser: undefined,
userAttributes: undefined,
selectPlacements: jest.fn(),
hashAttributes: jest.fn(),
setExtensionData: jest.fn(),
use: jest.fn(),
onShoppableAdsReady: jest.fn(),
terminate: jest.fn().mockResolvedValue(undefined),
});

it('should call kit.terminate when a kit and launcher are attached', async () => {
const kit = makeKit();
roktManager.attachKit(kit);

await roktManager.terminate();

expect(kit.terminate).toHaveBeenCalledTimes(1);
});

it('should await the kit terminate promise before resolving', async () => {
const kit = makeKit();
let settled = false;
(kit.terminate as jest.Mock).mockImplementation(
() => new Promise<void>(resolve => setTimeout(() => {
settled = true;
resolve();
}, 0))
);
roktManager.attachKit(kit);

await roktManager.terminate();

expect(settled).toBe(true);
});

it('should resolve as a no-op and not queue when no kit is attached', async () => {
await expect(roktManager.terminate()).resolves.toBeUndefined();

expect(roktManager['kit']).toBeNull();
expect(roktManager['messageQueue'].size).toBe(0);
expect(mockMPInstance.Logger.verbose).toHaveBeenCalledWith(
'mParticle.Rokt.terminate called before the Rokt kit was ready. Nothing to terminate.'
);
});

it('should resolve as a no-op when a kit is attached without a launcher', async () => {
const kit = makeKit();
kit.launcher = null;
roktManager.attachKit(kit);

await expect(roktManager.terminate()).resolves.toBeUndefined();

expect(kit.terminate).not.toHaveBeenCalled();
expect(roktManager['messageQueue'].size).toBe(0);
});

it('should log an error when the attached kit predates terminate support', async () => {
const kit = makeKit();
delete kit.terminate;
roktManager.attachKit(kit);

await expect(roktManager.terminate()).resolves.toBeUndefined();

expect(mockMPInstance.Logger.error).toHaveBeenCalledWith(
'mParticle.Rokt.terminate is not supported by the attached Rokt Kit version.'
);
});

it('should log an error and resolve when kit.terminate rejects', async () => {
const kit = makeKit();
(kit.terminate as jest.Mock).mockRejectedValue(new Error('teardown failed'));
roktManager.attachKit(kit);

await expect(roktManager.terminate()).resolves.toBeUndefined();

expect(mockMPInstance.Logger.error).toHaveBeenCalledWith(
'Failed to terminate the Rokt launcher: teardown failed'
);
});

it('should stay ready so repeat calls keep reaching the kit', async () => {
const kit = makeKit();
roktManager.attachKit(kit);

await roktManager.terminate();
await roktManager.terminate();

expect(roktManager.isReady()).toBe(true);
expect(kit.terminate).toHaveBeenCalledTimes(2);
});
});

describe('#onShoppableAdsReady', () => {
const makeKit = (): IRoktKit => ({
launcher: {
Expand Down
Loading