- Update kill-switch-client and feature-flag-client tests to use expect.objectContaining for headers to handle x-request-id - Move React Native SDK roadmap to completed/ Total: 44 client-side package tests passing
166 lines
4.5 KiB
TypeScript
166 lines
4.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { createFeatureFlagClient } from './client.js';
|
|
|
|
describe('createFeatureFlagClient', () => {
|
|
const baseConfig = {
|
|
baseUrl: 'http://localhost:4003/api',
|
|
productId: 'testapp',
|
|
platform: 'web',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should create a client with isEnabled returning false before init', () => {
|
|
const client = createFeatureFlagClient(baseConfig);
|
|
expect(client.isEnabled('any_flag')).toBe(false);
|
|
});
|
|
|
|
it('should fetch flags on init', async () => {
|
|
const mockFlags = { premium: true, beta_feature: false };
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: mockFlags }),
|
|
})
|
|
);
|
|
|
|
const client = createFeatureFlagClient(baseConfig);
|
|
await client.init();
|
|
|
|
expect(client.isEnabled('premium')).toBe(true);
|
|
expect(client.isEnabled('beta_feature')).toBe(false);
|
|
expect(client.isEnabled('nonexistent')).toBe(false);
|
|
client.stop();
|
|
});
|
|
|
|
it('should return all flags via getAllFlags', async () => {
|
|
const mockFlags = { a: true, b: false };
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: mockFlags }),
|
|
})
|
|
);
|
|
|
|
const client = createFeatureFlagClient(baseConfig);
|
|
await client.init();
|
|
|
|
expect(client.getAllFlags()).toEqual({ a: true, b: false });
|
|
client.stop();
|
|
});
|
|
|
|
it('should send correct headers', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: {} }),
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const client = createFeatureFlagClient(baseConfig);
|
|
await client.init({ userId: 'user-123' });
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
expect.stringContaining('/flags/poll?'),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({ 'x-product-id': 'testapp' }),
|
|
})
|
|
);
|
|
|
|
const url = fetchMock.mock.calls[0][0] as string;
|
|
expect(url).toContain('platform=web');
|
|
expect(url).toContain('userId=user-123');
|
|
client.stop();
|
|
});
|
|
|
|
it('should keep existing flags on network error', async () => {
|
|
let callCount = 0;
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockImplementation(() => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: { initial: true } }),
|
|
});
|
|
}
|
|
return Promise.reject(new Error('network'));
|
|
})
|
|
);
|
|
|
|
const client = createFeatureFlagClient(baseConfig);
|
|
await client.init();
|
|
expect(client.isEnabled('initial')).toBe(true);
|
|
|
|
await client.refresh();
|
|
expect(client.isEnabled('initial')).toBe(true);
|
|
client.stop();
|
|
});
|
|
|
|
it('should persist to storage when provided', async () => {
|
|
const store: Record<string, string> = {};
|
|
const storage = {
|
|
getItem: (k: string) => store[k] ?? null,
|
|
setItem: (k: string, v: string) => {
|
|
store[k] = v;
|
|
},
|
|
};
|
|
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: { cached: true } }),
|
|
})
|
|
);
|
|
|
|
const client = createFeatureFlagClient({ ...baseConfig, storage });
|
|
await client.init();
|
|
|
|
expect(store['testapp-feature-flags']).toBeDefined();
|
|
expect(JSON.parse(store['testapp-feature-flags'])).toEqual({ cached: true });
|
|
client.stop();
|
|
});
|
|
|
|
it('should restore flags from storage on creation', () => {
|
|
const store: Record<string, string> = {
|
|
'testapp-feature-flags': JSON.stringify({ restored: true }),
|
|
};
|
|
const storage = {
|
|
getItem: (k: string) => store[k] ?? null,
|
|
setItem: (k: string, v: string) => {
|
|
store[k] = v;
|
|
},
|
|
};
|
|
|
|
const client = createFeatureFlagClient({ ...baseConfig, storage });
|
|
expect(client.isEnabled('restored')).toBe(true);
|
|
});
|
|
|
|
it('should stop polling on stop()', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ flags: {} }),
|
|
})
|
|
);
|
|
|
|
const client = createFeatureFlagClient({ ...baseConfig, pollIntervalMs: 1000 });
|
|
await client.init();
|
|
client.stop();
|
|
|
|
expect(client.isEnabled('anything')).toBe(false);
|
|
expect(client.getAllFlags()).toEqual({});
|
|
});
|
|
});
|