Packages added: - @bytelyst/referral-client — referral API client + share helpers - @bytelyst/subscription-client — subscription/plan API client + cache - @bytelyst/celebrations — milestone triggers, confetti, positive messages - @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases - @bytelyst/accessibility — VoiceOver/TalkBack label generators - @bytelyst/quick-actions — progressive disclosure, smart defaults - @bytelyst/time-references — familiar duration references - @bytelyst/org-client — org/workspace/membership/license API client - @bytelyst/marketplace-client — listing/review/install API client All packages: pure TS, ESM, globalThis.fetch, no Node.js deps. 99 Vitest tests across 9 packages, 79/79 public methods covered. Review fixes applied: - time-references: fix module-level mutable state leak + add clearCustomReferences() - accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords - subscription-client: fix flaky daysRemaining test (ms boundary race)
225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { createReferralClient } from './client.js';
|
|
import type { ReferralDoc } from './types.js';
|
|
|
|
const baseConfig = {
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
getAccessToken: () => 'test-token',
|
|
};
|
|
|
|
function mockReferral(overrides?: Partial<ReferralDoc>): ReferralDoc {
|
|
return {
|
|
id: 'ref-1',
|
|
productId: 'testapp',
|
|
referrerId: 'user-1',
|
|
referrerEmail: 'alice@test.com',
|
|
referredUserId: null,
|
|
referredEmail: 'bob@test.com',
|
|
status: 'pending',
|
|
referrerRewardTokens: 1000,
|
|
referredRewardTokens: 500,
|
|
referrerRewarded: false,
|
|
referredRewarded: false,
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
completedAt: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('createReferralClient', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should create a referral', async () => {
|
|
const ref = mockReferral();
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(ref),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.createReferral({
|
|
referrerId: 'user-1',
|
|
referrerEmail: 'alice@test.com',
|
|
referredEmail: 'bob@test.com',
|
|
});
|
|
|
|
expect(result).toEqual(ref);
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/referrals',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
});
|
|
|
|
it('should list referrals by referrerId', async () => {
|
|
const data = { referrals: [mockReferral()], count: 1 };
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(data),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.listMyReferrals('user-1');
|
|
|
|
expect(result.count).toBe(1);
|
|
expect(result.referrals).toHaveLength(1);
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/referrals/by-referrer/user-1',
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it('should get referral stats', async () => {
|
|
const stats = { total: 10, completed: 5, rewarded: 3 };
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(stats),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.getReferralStats();
|
|
expect(result).toEqual(stats);
|
|
});
|
|
|
|
it('should update referral status via PUT', async () => {
|
|
const updated = mockReferral({ status: 'signed_up' });
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(updated),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.updateReferralStatus('ref-1', 'user-1', 'signed_up');
|
|
|
|
expect(result.status).toBe('signed_up');
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://localhost:4003/api/referrals/ref-1',
|
|
expect.objectContaining({ method: 'PUT' })
|
|
);
|
|
});
|
|
|
|
it('should get referral by email', async () => {
|
|
const ref = mockReferral();
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(ref),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.getByEmail('bob@test.com');
|
|
expect(result).toEqual(ref);
|
|
});
|
|
|
|
it('should return null for 404 on getByEmail', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 404,
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
const result = await client.getByEmail('unknown@test.com');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should send correct headers', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ total: 0, completed: 0, rewarded: 0 }),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
await client.getReferralStats();
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
const callHeaders = fetchMock.mock.calls[0][1].headers as Record<string, string>;
|
|
expect(callHeaders['x-product-id']).toBe('testapp');
|
|
expect(callHeaders['Authorization']).toBe('Bearer test-token');
|
|
expect(callHeaders['x-request-id']).toBeDefined();
|
|
});
|
|
|
|
it('should throw on non-ok response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient(baseConfig);
|
|
await expect(client.getReferralStats()).rejects.toThrow('getReferralStats failed: 500');
|
|
});
|
|
|
|
it('should build share link', () => {
|
|
const client = createReferralClient(baseConfig);
|
|
const link = client.buildShareLink('ABC123');
|
|
expect(link).toContain('refer/ABC123');
|
|
expect(link).toContain('product=testapp');
|
|
});
|
|
|
|
it('should build share message', () => {
|
|
const client = createReferralClient(baseConfig);
|
|
const msg = client.buildShareMessage('ABC123', 'NomGap');
|
|
expect(msg).toContain('NomGap');
|
|
expect(msg).toContain('refer/ABC123');
|
|
});
|
|
|
|
it('should calculate earned days', () => {
|
|
const client = createReferralClient(baseConfig);
|
|
expect(client.calculateEarnedDays(3)).toBe(21);
|
|
expect(client.calculateEarnedDays(0)).toBe(0);
|
|
expect(client.calculateEarnedDays(2, 14)).toBe(28);
|
|
});
|
|
|
|
it('should use custom default reward tokens', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockReferral()),
|
|
})
|
|
);
|
|
|
|
const client = createReferralClient({
|
|
...baseConfig,
|
|
defaultRewardTokens: { referrer: 2000, referred: 1000 },
|
|
});
|
|
await client.createReferral({
|
|
referrerId: 'user-1',
|
|
referrerEmail: 'alice@test.com',
|
|
referredEmail: 'bob@test.com',
|
|
});
|
|
|
|
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
|
|
expect(body.referrerRewardTokens).toBe(2000);
|
|
expect(body.referredRewardTokens).toBe(1000);
|
|
});
|
|
});
|