Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
cf3131a
WIP on goals.
kinyoklion Sep 17, 2024
543cfc9
Add tests for GoalTracker.
kinyoklion Sep 17, 2024
0288923
Add goal manager tests.
kinyoklion Sep 17, 2024
56684f7
Add options for goals and fix bugs.
kinyoklion Sep 18, 2024
97a90ed
Merge branch 'main' into rlamb/sc-254419/implement-goals
kinyoklion Sep 18, 2024
f30141a
Simplify tests.
kinyoklion Sep 18, 2024
40f78fe
Lint browser package.
kinyoklion Sep 18, 2024
2ff006c
Lint
kinyoklion Sep 18, 2024
a11e73d
Add tests for location watcher.
kinyoklion Sep 18, 2024
7067a28
WIP: Implement support for event URLs.
kinyoklion Sep 18, 2024
fdf4a43
feat: Add URLs for custom events and URL filtering.
kinyoklion Sep 19, 2024
29cd72a
Merge branch 'main' into rlamb/sdk-10/support-event-urls
kinyoklion Sep 19, 2024
2f730ab
Revert jest.config.js
kinyoklion Sep 19, 2024
dbfe431
Remove duplicate merged code.
kinyoklion Sep 19, 2024
57e835d
Remove unused import.
kinyoklion Sep 19, 2024
bcb3c4d
WIP: Refactor data handling.
kinyoklion Sep 19, 2024
0695c74
WIP: JS style initialization.
kinyoklion Sep 20, 2024
1439258
Add correct typing to createIdentifyPromise.
kinyoklion Sep 20, 2024
312fb49
Basic data manager functioning.
kinyoklion Sep 20, 2024
4318393
Basic functionality.
kinyoklion Sep 20, 2024
dd0e5fb
fix: Ensure browser contract tests run during top-level build.
kinyoklion Sep 23, 2024
f35f804
WIP: js-style-initialization
kinyoklion Sep 23, 2024
e020e86
Merge remote-tracking branch 'origin/rlamb/fix-browser-contract-test-…
kinyoklion Sep 23, 2024
80b5b61
Disable goals.
kinyoklion Sep 23, 2024
a83184d
Fix identify options.
kinyoklion Sep 23, 2024
c3cbd46
Merge remote-tracking branch 'origin/rlamb/fix-browser-contract-test-…
kinyoklion Sep 23, 2024
bba88a1
Add todo.
kinyoklion Sep 23, 2024
22b89a9
WIP
kinyoklion Sep 23, 2024
d1a59c8
Lint
kinyoklion Sep 23, 2024
60ea015
Fix tests.
kinyoklion Sep 23, 2024
2bdc298
Testing progress.
kinyoklion Sep 23, 2024
16b7732
More tests
kinyoklion Sep 24, 2024
fda525e
Lint.
kinyoklion Sep 24, 2024
b3b724f
Merge branch 'main' into rlamb/sdk-195/support-js-style-initialization
kinyoklion Sep 24, 2024
7722641
Cleanup sdk client tests.
kinyoklion Sep 24, 2024
045a784
Allow shared test code.
kinyoklion Sep 24, 2024
b097038
Cleanup imports
kinyoklion Sep 24, 2024
72139de
Revert event processor changes and disable auto-start for client SDKs.
kinyoklion Sep 24, 2024
6951966
Add log tag for mobile data manager.
kinyoklion Sep 24, 2024
3f1bb24
Remove pointless docs.
kinyoklion Sep 24, 2024
aa33fa4
Correct docs.
kinyoklion Sep 24, 2024
0da9d31
Change option to streaming.
kinyoklion Sep 24, 2024
3b02890
PR Feedback.
kinyoklion Sep 25, 2024
a713d54
Lint
kinyoklion Sep 25, 2024
3c70a56
Fix bad construction
kinyoklion Sep 25, 2024
55d9b6b
More configuratin fixes.
kinyoklion Sep 25, 2024
eb21120
Add developer note for identifyResolve/identifyReject.
kinyoklion Sep 25, 2024
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
22 changes: 5 additions & 17 deletions packages/sdk/browser/__tests__/BrowserClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
AutoEnvAttributes,
EventSourceCapabilities,
EventSourceInitDict,
Hasher,
LDLogger,
PlatformData,
Requests,
SdkData,
} from '@launchdarkly/js-client-sdk-common';

import { BrowserClient } from '../src/BrowserClient';
import { MockHasher } from './MockHasher';

function mockResponse(value: string, statusCode: number) {
const response: Response = {
Expand Down Expand Up @@ -79,18 +79,6 @@ function makeRequests(): Requests {
};
}

class MockHasher implements Hasher {
update(_data: string): Hasher {
return this;
}
digest?(_encoding: string): string {
return 'hashed';
}
async asyncDigest?(_encoding: string): Promise<string> {
return 'hashed';
}
}

describe('given a mock platform for a BrowserClient', () => {
const logger: LDLogger = {
debug: jest.fn(),
Expand Down Expand Up @@ -141,7 +129,7 @@ describe('given a mock platform for a BrowserClient', () => {
'client-side-id',
AutoEnvAttributes.Disabled,
{
initialConnectionMode: 'polling',
stream: false,
logger,
diagnosticOptOut: true,
},
Expand Down Expand Up @@ -169,7 +157,7 @@ describe('given a mock platform for a BrowserClient', () => {
'client-side-id',
AutoEnvAttributes.Disabled,
{
initialConnectionMode: 'polling',
stream: false,
logger,
diagnosticOptOut: true,
eventUrlTransformer: (url: string) =>
Expand Down Expand Up @@ -202,7 +190,7 @@ describe('given a mock platform for a BrowserClient', () => {
'client-side-id',
AutoEnvAttributes.Disabled,
{
initialConnectionMode: 'polling',
stream: false,
logger,
diagnosticOptOut: true,
eventUrlTransformer: (url: string) =>
Expand Down Expand Up @@ -245,7 +233,7 @@ describe('given a mock platform for a BrowserClient', () => {
'client-side-id',
AutoEnvAttributes.Disabled,
{
initialConnectionMode: 'polling',
stream: false,
logger,
diagnosticOptOut: true,
eventUrlTransformer: (url: string) =>
Expand Down
301 changes: 301 additions & 0 deletions packages/sdk/browser/__tests__/BrowserDataManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import { jest } from '@jest/globals';
import { TextEncoder } from 'node:util';

import {
ApplicationTags,
base64UrlEncode,
Configuration,
Context,
Encoding,
FlagManager,
internal,
LDEmitter,
LDHeaders,
LDIdentifyOptions,
LDLogger,
Platform,
Response,
ServiceEndpoints,
} from '@launchdarkly/js-client-sdk-common';

import BrowserDataManager from '../src/BrowserDataManager';
import validateOptions, { ValidatedOptions } from '../src/options';
import BrowserEncoding from '../src/platform/BrowserEncoding';
import BrowserInfo from '../src/platform/BrowserInfo';
import LocalStorage from '../src/platform/LocalStorage';
import { MockHasher } from './MockHasher';

global.TextEncoder = TextEncoder;

function mockResponse(value: string, statusCode: number) {
const response: Response = {
headers: {
// @ts-ignore
get: jest.fn(),
// @ts-ignore
keys: jest.fn(),
// @ts-ignore
values: jest.fn(),
// @ts-ignore
entries: jest.fn(),
// @ts-ignore
has: jest.fn(),
},
status: statusCode,
text: () => Promise.resolve(value),
json: () => Promise.resolve(JSON.parse(value)),
};
return Promise.resolve(response);
}

/**
* Mocks fetch. Returns the fetch jest.Mock object.
* @param remoteJson
* @param statusCode
*/
function mockFetch(value: string, statusCode: number = 200) {
const f = jest.fn();
// @ts-ignore
f.mockResolvedValue(mockResponse(value, statusCode));
return f;
}

describe('given a BrowserDataManager with mocked dependencies', () => {
let platform: jest.Mocked<Platform>;
let flagManager: jest.Mocked<FlagManager>;
let config: Configuration;
let browserConfig: ValidatedOptions;
let baseHeaders: LDHeaders;
let emitter: jest.Mocked<LDEmitter>;
let diagnosticsManager: jest.Mocked<internal.DiagnosticsManager>;
let dataManager: BrowserDataManager;
let logger: LDLogger;
beforeEach(() => {
logger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
config = {
logger,
baseUri: 'string',
eventsUri: 'string',
streamUri: 'string',
maxCachedContexts: 5,
capacity: 100,
diagnosticRecordingInterval: 1000,
flushInterval: 1000,
streamInitialReconnectDelay: 1000,
allAttributesPrivate: false,
debug: true,
diagnosticOptOut: false,
sendEvents: false,
sendLDHeaders: true,
useReport: false,
withReasons: true,
privateAttributes: [],
tags: new ApplicationTags({}),
serviceEndpoints: new ServiceEndpoints('', ''),
pollInterval: 1000,
userAgentHeaderName: 'user-agent',
trackEventModifier: (event) => event,
};
const mockedFetch = mockFetch('{"flagA": true}', 200);
platform = {
crypto: {
createHash: () => new MockHasher(),
randomUUID: () => '123',
},
info: new BrowserInfo(),
requests: {
createEventSource: jest.fn((streamUri: string = '', options: any = {}) => ({
streamUri,
options,
onclose: jest.fn(),
addEventListener: jest.fn(),
close: jest.fn(),
})),
fetch: mockedFetch,
getEventSourceCapabilities: jest.fn(),
},
storage: new LocalStorage(config.logger),
encoding: new BrowserEncoding(),
} as unknown as jest.Mocked<Platform>;

flagManager = {
loadCached: jest.fn(),
get: jest.fn(),
getAll: jest.fn(),
init: jest.fn(),
upsert: jest.fn(),
on: jest.fn(),
off: jest.fn(),
} as unknown as jest.Mocked<FlagManager>;

browserConfig = validateOptions({ stream: false }, logger);
baseHeaders = {};
emitter = {
emit: jest.fn(),
} as unknown as jest.Mocked<LDEmitter>;
diagnosticsManager = {} as unknown as jest.Mocked<internal.DiagnosticsManager>;

dataManager = new BrowserDataManager(
platform,
flagManager,
'test-credential',
config,
browserConfig,
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
},
}),
baseHeaders,
emitter,
diagnosticsManager,
);
});

afterEach(() => {
jest.resetAllMocks();
});

it('creates an event source when stream is true', async () => {
dataManager = new BrowserDataManager(
platform,
flagManager,
'test-credential',
config,
validateOptions({ stream: true }, logger),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/msdk/evalx/context`;
},
}),
() => ({
pathGet(encoding: Encoding, _plainContextString: string): string {
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
},
pathReport(_encoding: Encoding, _plainContextString: string): string {
return `/meval`;
},
}),
baseHeaders,
emitter,
diagnosticsManager,
);

const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(platform.requests.createEventSource).toHaveBeenCalled();
});

it('should load cached flags and continue to poll to complete identify', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(true);

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(logger.debug).toHaveBeenCalledWith(
'[BrowserDataManager] Identify - Flags loaded from cache. Continuing to initialize via a poll.',
);

expect(flagManager.loadCached).toHaveBeenCalledWith(context);
expect(identifyResolve).toHaveBeenCalled();
expect(flagManager.init).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
);
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
});

it('should identify from polling when there are no cached flags', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(false);

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(logger.debug).not.toHaveBeenCalledWith(
'Identify - Flags loaded from cache. Continuing to initialize via a poll.',
);

expect(flagManager.loadCached).toHaveBeenCalledWith(context);
expect(identifyResolve).toHaveBeenCalled();
expect(flagManager.init).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
);
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
});

it('creates a stream when streaming is enabled after construction', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(false);

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(platform.requests.createEventSource).not.toHaveBeenCalled();
dataManager.startDataSource();
expect(platform.requests.createEventSource).toHaveBeenCalled();
});

it('does not re-create the stream if it already running', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(false);

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);

expect(platform.requests.createEventSource).not.toHaveBeenCalled();
dataManager.startDataSource();
dataManager.startDataSource();
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith(
'[BrowserDataManager] Update processor already active. Not changing state.',
);
});

it('does not start a stream if identify has not been called', async () => {
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
dataManager.startDataSource();
expect(platform.requests.createEventSource).not.toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith(
'[BrowserDataManager] Context not set, not starting update processor.',
);
});
});
13 changes: 13 additions & 0 deletions packages/sdk/browser/__tests__/MockHasher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Hasher } from '@launchdarkly/js-client-sdk-common';

export class MockHasher implements Hasher {
update(_data: string): Hasher {
return this;
}
digest?(_encoding: string): string {
return 'hashed';
}
async asyncDigest?(_encoding: string): Promise<string> {
return 'hashed';
}
}
Loading
Loading