Files
rudder-js-sdk/test/e2e/boundary.e2e.test.ts
T

127 lines
4.5 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { getScenarioRuntime, RudderClient } from '../../src/client/RudderClient.js';
import { createFakeTokenStore } from '../helpers/FakeTokenStore.js';
import { createFakeGateway, type FakeGateway } from '../helpers/fakeGateway.js';
import { stubDeterministicUuid } from '../helpers/memoryPlanStore.js';
import { effect } from '../helpers/plan.js';
import type { StoreOfferEffect } from '../../src/index.js';
function makeClient(): RudderClient {
return new RudderClient({
baseUrl: 'https://api.test.rudder.build',
projectKey: 'test-project',
tokenStore: createFakeTokenStore(),
});
}
describe('E2E: boundary nodes (server-side continuation)', () => {
let gateway: FakeGateway;
let client: RudderClient;
beforeEach(() => {
stubDeterministicUuid();
gateway = createFakeGateway({
stores: [{
slug: 'starter',
name: 'Starter',
offers: [{ id: 'pack_1', slug: 'pack-1', name: 'Starter Pack' }],
}],
});
gateway.install();
client = makeClient();
});
afterEach(() => vi.unstubAllGlobals());
it('purchase crosses the boundary → callback fires and continues the scenario', async () => {
const offerEffect = effect('store', { storeSlug: 'starter', message: 'Buy the starter pack!' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
const reward = effect('notification', { message: 'Reward granted: 500 gems!' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'reward',
});
gateway.onEvent('player_login', offerEffect);
gateway.onCallback('offer', 'onPurchase', reward);
const messages: string[] = [];
const completed: string[] = [];
let offer: StoreOfferEffect | undefined;
client.effects.onStoreOffer((e) => { offer = e; });
client.effects.onNotification((e) => { messages.push(e.message); });
client.effects.onScenarioCompleted((e) => { completed.push(e.scenarioSlug); });
const token = (await client.auth.loginWithDevice()).accessToken;
await vi.waitFor(() => expect(offer).toBeDefined());
await offer!.buy(offer!.offers[0]);
const callback = gateway.recorded.find((r) => r.path === '/sdk/v1/scenarios/callback');
expect(callback?.body).toEqual({
scenarioSlug: 'offer_flow',
nodeId: 'offer',
handle: 'onPurchase',
runId: 'offer_flow-run',
});
expect(callback?.authToken).toBe(token);
expect(messages).toEqual(['Reward granted: 500 gems!']);
expect(messages).not.toContain('Maybe next time!');
expect(completed).toEqual([]);
});
it('decline posts callback and continues with the server-supplied next effect', async () => {
const offerEffect = effect('store', { storeSlug: 'starter', message: 'Buy the starter pack!' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
const consolation = effect('notification', { message: 'Maybe next time!' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'consolation',
});
gateway.onEvent('player_login', offerEffect);
gateway.onCallback('offer', 'onDecline', consolation);
const messages: string[] = [];
let offer: StoreOfferEffect | undefined;
client.effects.onStoreOffer((e) => { offer = e; });
client.effects.onNotification((e) => { messages.push(e.message); });
await client.auth.loginWithDevice();
await vi.waitFor(() => expect(offer).toBeDefined());
await offer!.dismiss();
expect(gateway.recorded.some((r) => r.path === '/sdk/v1/scenarios/callback')).toBe(true);
expect(messages).toEqual(['Maybe next time!']);
});
it('a failing callback does not fail or crash the run', async () => {
const offerEffect = effect('store', { storeSlug: 'starter' }, {
scenarioSlug: 'offer_flow',
runId: 'offer_flow-run',
nodeId: 'offer',
});
gateway.onEvent('player_login', offerEffect);
gateway.onCallbackError('offer', 'onPurchase', 500);
const failures: unknown[] = [];
let offer: StoreOfferEffect | undefined;
const runtime = await getScenarioRuntime(client);
client.effects.onStoreOffer((e) => { offer = e; });
client.effects.onScenarioFailed((e) => { failures.push(e); });
await client.auth.loginWithDevice();
await vi.waitFor(() => expect(offer).toBeDefined());
await expect(offer!.buy(offer!.offers[0])).resolves.toMatchObject({ success: true });
expect(failures).toHaveLength(0);
expect(runtime.isRunning).toBe(true);
});
});