Skip to content

Commit 014b59b

Browse files
authored
chore: adding predefined sdk wrappers for test data (#1413)
This PR will create test data wrappers for `js-client-sdk` and `react-sdk` to make the tooling more ergonomic. This PR will also pull in the eslint upgrade. NOTE: we did not wrap the `react-native-sdk` test data wrapper because unit testing react native in this way requires a much more robust setup that we are not pursuing yet. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Test-only tooling and package exports with no production SDK behavior changes; ESLint upgrade is confined to this package. > > **Overview** > Adds ergonomic **offline test helpers** for `@launchdarkly/js-client-sdk` and `@launchdarkly/react-sdk` via new subpath exports `@launchdarkly/client-testing-plugin/js-client-sdk` and `.../react-sdk`. > > Each **`createTestClient`** builds a client with `TestData` appended after any user `plugins`, plus `sendEvents: false` and `streaming: false`, and returns `{ client, testData }` so tests can seed `initialFlags` and mutate flags at runtime. The React entry also exposes **`createTestClientProvider`**, which pairs the same client with a pre-wired Provider from `createLDReactProviderWithClient`. > > **Build and packaging:** `tsup` now emits the two client modules; **optional peer dependencies** on the JS and React SDKs were added so consumers import only what they need. **Jest** coverage verifies seeding, plugin composition, dynamic overrides, and the React Provider helper. Lint tooling in this package was bumped to **ESLint 9** with updated scripts. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c790a1a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 93bd3f8 commit 014b59b

6 files changed

Lines changed: 269 additions & 8 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { createTestClient } from '../../src/clients/js-client-sdk';
5+
import TestData from '../../src/TestData';
6+
7+
afterEach(async () => {
8+
// close any leaked clients via global registry would be nicer; for now,
9+
// each test that constructs a client is responsible for closing it.
10+
});
11+
12+
it('seeds the TestData with initialFlags', async () => {
13+
const { client, testData } = createTestClient(
14+
{ kind: 'user', key: 'tester' },
15+
{ 'bool-flag': true, greeting: 'hello' },
16+
);
17+
await client.start({ bootstrap: {} });
18+
19+
expect(testData).toBeInstanceOf(TestData);
20+
expect(client.boolVariation('bool-flag', false)).toBe(true);
21+
expect(client.stringVariation('greeting', 'default')).toBe('hello');
22+
23+
await client.close();
24+
});
25+
26+
it('appends TestData to user-supplied plugins rather than replacing them', async () => {
27+
const userPluginRegisterDebug = jest.fn();
28+
const userPlugin = {
29+
getMetadata: () => ({ name: 'user-plugin' }),
30+
register: jest.fn(),
31+
registerDebug: userPluginRegisterDebug,
32+
};
33+
34+
const { client, testData } = createTestClient(
35+
{ kind: 'user', key: 'tester' },
36+
{ 'bool-flag': true },
37+
{ plugins: [userPlugin] },
38+
);
39+
await client.start({ bootstrap: {} });
40+
41+
expect(userPluginRegisterDebug).toHaveBeenCalled();
42+
expect(client.boolVariation('bool-flag', false)).toBe(true);
43+
// testData is still the TestData we returned, not shadowed by the user plugin
44+
expect(testData).toBeInstanceOf(TestData);
45+
46+
await client.close();
47+
});
48+
49+
it('updates flag values dynamically via testData after the client is started', async () => {
50+
const { client, testData } = createTestClient(
51+
{ kind: 'user', key: 'tester' },
52+
{ 'show-banner': true },
53+
);
54+
await client.start({ bootstrap: {} });
55+
56+
expect(client.boolVariation('show-banner', false)).toBe(true);
57+
58+
testData.setBool('show-banner', false);
59+
expect(client.boolVariation('show-banner', true)).toBe(false);
60+
61+
await client.close();
62+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { createTestClient, createTestClientProvider } from '../../src/clients/react-sdk';
5+
import TestData from '../../src/TestData';
6+
7+
it('seeds the TestData with initialFlags and resolves overrides after start', async () => {
8+
const { client, testData } = createTestClient(
9+
{ kind: 'user', key: 'tester' },
10+
{ 'bool-flag': true, greeting: 'hello' },
11+
);
12+
await client.start({ bootstrap: {} });
13+
14+
expect(testData).toBeInstanceOf(TestData);
15+
expect(client.boolVariation('bool-flag', false)).toBe(true);
16+
expect(client.stringVariation('greeting', 'default')).toBe('hello');
17+
18+
await client.close();
19+
});
20+
21+
it('appends TestData to user-supplied plugins rather than replacing them', async () => {
22+
const userPluginRegisterDebug = jest.fn();
23+
const userPlugin = {
24+
getMetadata: () => ({ name: 'user-plugin' }),
25+
register: jest.fn(),
26+
registerDebug: userPluginRegisterDebug,
27+
};
28+
29+
const { client } = createTestClient(
30+
{ kind: 'user', key: 'tester' },
31+
{ 'bool-flag': true },
32+
{ plugins: [userPlugin] },
33+
);
34+
await client.start({ bootstrap: {} });
35+
36+
expect(userPluginRegisterDebug).toHaveBeenCalled();
37+
expect(client.boolVariation('bool-flag', false)).toBe(true);
38+
39+
await client.close();
40+
});
41+
42+
it('createTestClientProvider returns a pre-wired Provider, client, and testData', () => {
43+
const { Provider, client, testData } = createTestClientProvider(
44+
{ kind: 'user', key: 'tester' },
45+
{ 'show-banner': true },
46+
);
47+
expect(Provider).toBeInstanceOf(Function);
48+
expect(client).toBeDefined();
49+
expect(testData).toBeInstanceOf(TestData);
50+
});
51+
52+
it('updates flag values dynamically via testData after the client is started', async () => {
53+
const { client, testData } = createTestClient(
54+
{ kind: 'user', key: 'tester' },
55+
{ 'show-banner': true },
56+
);
57+
await client.start({ bootstrap: {} });
58+
59+
expect(client.boolVariation('show-banner', false)).toBe(true);
60+
61+
testData.setBool('show-banner', false);
62+
expect(client.boolVariation('show-banner', true)).toBe(false);
63+
64+
await client.close();
65+
});

packages/tooling/client-testing-plugin/package.json

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
"import": "./dist/index.js",
2424
"require": "./dist/index.cjs",
2525
"default": "./dist/index.js"
26+
},
27+
"./js-client-sdk": {
28+
"types": "./dist/clients/js-client-sdk.d.ts",
29+
"import": "./dist/clients/js-client-sdk.js",
30+
"require": "./dist/clients/js-client-sdk.cjs",
31+
"default": "./dist/clients/js-client-sdk.js"
32+
},
33+
"./react-sdk": {
34+
"types": "./dist/clients/react-sdk.d.ts",
35+
"import": "./dist/clients/react-sdk.js",
36+
"require": "./dist/clients/react-sdk.cjs",
37+
"default": "./dist/clients/react-sdk.js"
2638
}
2739
},
2840
"files": [
@@ -32,24 +44,40 @@
3244
"clean": "rimraf dist",
3345
"build": "tsup",
3446
"test": "npx jest --ci",
35-
"lint": "eslint . --ext .ts,.tsx"
47+
"lint": "eslint .",
48+
"lint:fix": "yarn run lint --fix"
3649
},
3750
"dependencies": {
3851
"@launchdarkly/js-client-sdk-common": "workspace:^"
3952
},
53+
"peerDependencies": {
54+
"@launchdarkly/js-client-sdk": "workspace:^",
55+
"@launchdarkly/react-sdk": "workspace:^"
56+
},
57+
"peerDependenciesMeta": {
58+
"@launchdarkly/js-client-sdk": {
59+
"optional": true
60+
},
61+
"@launchdarkly/react-sdk": {
62+
"optional": true
63+
}
64+
},
4065
"devDependencies": {
66+
"@eslint/js": "^9.0.0",
4167
"@launchdarkly/js-client-sdk": "workspace:^",
68+
"@launchdarkly/react-sdk": "workspace:^",
4269
"@types/jest": "^29.5.0",
43-
"@typescript-eslint/eslint-plugin": "^6.20.0",
44-
"@typescript-eslint/parser": "^6.20.0",
45-
"eslint": "^8.45.0",
46-
"eslint-plugin-import": "^2.27.5",
47-
"eslint-plugin-jest": "^27.6.3",
70+
"eslint": "^9.0.0",
71+
"eslint-import-resolver-typescript": "^4.0.0",
72+
"eslint-plugin-import-x": "^4.0.0",
73+
"eslint-plugin-jest": "^28.0.0",
74+
"globals": "^16.0.0",
4875
"jest": "^30.2.0",
4976
"jest-environment-jsdom": "^30.0.0",
5077
"rimraf": "6.0.1",
5178
"ts-jest": "^29.1.1",
5279
"tsup": "^8.5.1",
53-
"typescript": "5.1.6"
80+
"typescript": "5.1.6",
81+
"typescript-eslint": "^8.0.0"
5482
}
5583
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { LDFlagValue } from '@launchdarkly/js-client-sdk-common';
2+
import type { LDClient, LDContext, LDOptions } from '@launchdarkly/js-client-sdk';
3+
import { createClient } from '@launchdarkly/js-client-sdk';
4+
5+
import TestData from '../TestData';
6+
7+
const TEST_CLIENT_SIDE_ID = 'client-testing-plugin';
8+
9+
export interface CreateTestClientResult {
10+
client: LDClient;
11+
testData: TestData;
12+
}
13+
14+
/**
15+
* Creates a `@launchdarkly/js-client-sdk` client wired with the `TestData`
16+
* plugin and the settings required for offline test usage.
17+
*
18+
* @param context the LDContext to identify the test client as
19+
* @param initialFlags optional seed map of flag keys to values
20+
* @param options optional LDOptions
21+
*
22+
* @returns an object containing the test client and test data
23+
*/
24+
export function createTestClient(
25+
context: LDContext,
26+
initialFlags?: { [key: string]: LDFlagValue },
27+
options?: Partial<LDOptions>,
28+
): CreateTestClientResult {
29+
const testData = new TestData(initialFlags);
30+
const userPlugins = options?.plugins ?? [];
31+
const client = createClient(TEST_CLIENT_SIDE_ID, context, {
32+
...options,
33+
plugins: [...userPlugins, testData],
34+
sendEvents: false,
35+
streaming: false,
36+
});
37+
return { client, testData };
38+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { LDFlagValue } from '@launchdarkly/js-client-sdk-common';
2+
import type { LDContext, LDReactClient, LDReactClientOptions } from '@launchdarkly/react-sdk';
3+
import { createClient, createLDReactProviderWithClient } from '@launchdarkly/react-sdk';
4+
5+
import TestData from '../TestData';
6+
7+
const TEST_CLIENT_SIDE_ID = 'client-testing-plugin';
8+
9+
export interface CreateTestClientResult {
10+
client: LDReactClient;
11+
testData: TestData;
12+
}
13+
14+
export interface CreateTestClientProviderResult {
15+
Provider: ReturnType<typeof createLDReactProviderWithClient>;
16+
client: LDReactClient;
17+
testData: TestData;
18+
}
19+
20+
/**
21+
* Creates a `@launchdarkly/react-sdk` client wired with the `TestData` plugin
22+
* and the settings required for offline test usage.
23+
*
24+
* @param context the LDContext to identify the test client as
25+
* @param initialFlags optional seed map of flag keys to values
26+
* @param options optional react-sdk options
27+
*
28+
* @returns an object containing the test client and test data
29+
*/
30+
export function createTestClient(
31+
context: LDContext,
32+
initialFlags?: { [key: string]: LDFlagValue },
33+
options?: Partial<LDReactClientOptions>,
34+
): CreateTestClientResult {
35+
const testData = new TestData(initialFlags);
36+
const userPlugins = options?.plugins ?? [];
37+
const client = createClient(TEST_CLIENT_SIDE_ID, context, {
38+
...options,
39+
plugins: [...userPlugins, testData],
40+
sendEvents: false,
41+
streaming: false,
42+
});
43+
return { client, testData };
44+
}
45+
46+
/**
47+
* Creates a `@launchdarkly/react-sdk` client and a pre-wired Provider component,
48+
* ready to wrap components under test.
49+
*
50+
* @param context the LDContext to identify the test client as
51+
* @param initialFlags optional seed map of flag keys to values
52+
* @param options optional react-sdk options
53+
*
54+
* @returns an object containing the Provider component, client, and testData
55+
*/
56+
export function createTestClientProvider(
57+
context: LDContext,
58+
initialFlags?: { [key: string]: LDFlagValue },
59+
options?: Partial<LDReactClientOptions>,
60+
): CreateTestClientProviderResult {
61+
const { client, testData } = createTestClient(context, initialFlags, options);
62+
const Provider = createLDReactProviderWithClient(client);
63+
return { Provider, client, testData };
64+
}

packages/tooling/client-testing-plugin/tsup.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { defineConfig } from 'tsup';
22

33
export default defineConfig([
44
{
5-
entry: { index: 'src/index.ts' },
5+
entry: {
6+
'index': 'src/index.ts',
7+
'clients/js-client-sdk': 'src/clients/js-client-sdk.ts',
8+
'clients/react-sdk': 'src/clients/react-sdk.ts',
9+
},
610
format: ['esm', 'cjs'],
711
dts: true,
812
clean: true,

0 commit comments

Comments
 (0)