From c790a1a9166becf3d6bc853a4edae869c85b1d00 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 3 Jun 2026 16:39:01 -0400 Subject: [PATCH] chore: adding predefined sdk wrappers for test data NOTE: we removed the `react-native-sdk` test data wrapper because it is not effective. --- .../__tests__/clients/js-client-sdk.test.ts | 62 ++++++++++++++++++ .../__tests__/clients/react-sdk.test.ts | 65 +++++++++++++++++++ .../client-testing-plugin/package.json | 42 ++++++++++-- .../src/clients/js-client-sdk.ts | 38 +++++++++++ .../src/clients/react-sdk.ts | 64 ++++++++++++++++++ .../client-testing-plugin/tsup.config.js | 6 +- 6 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 packages/tooling/client-testing-plugin/__tests__/clients/js-client-sdk.test.ts create mode 100644 packages/tooling/client-testing-plugin/__tests__/clients/react-sdk.test.ts create mode 100644 packages/tooling/client-testing-plugin/src/clients/js-client-sdk.ts create mode 100644 packages/tooling/client-testing-plugin/src/clients/react-sdk.ts diff --git a/packages/tooling/client-testing-plugin/__tests__/clients/js-client-sdk.test.ts b/packages/tooling/client-testing-plugin/__tests__/clients/js-client-sdk.test.ts new file mode 100644 index 0000000000..d8b062cdd2 --- /dev/null +++ b/packages/tooling/client-testing-plugin/__tests__/clients/js-client-sdk.test.ts @@ -0,0 +1,62 @@ +/** + * @jest-environment jsdom + */ +import { createTestClient } from '../../src/clients/js-client-sdk'; +import TestData from '../../src/TestData'; + +afterEach(async () => { + // close any leaked clients via global registry would be nicer; for now, + // each test that constructs a client is responsible for closing it. +}); + +it('seeds the TestData with initialFlags', async () => { + const { client, testData } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'bool-flag': true, greeting: 'hello' }, + ); + await client.start({ bootstrap: {} }); + + expect(testData).toBeInstanceOf(TestData); + expect(client.boolVariation('bool-flag', false)).toBe(true); + expect(client.stringVariation('greeting', 'default')).toBe('hello'); + + await client.close(); +}); + +it('appends TestData to user-supplied plugins rather than replacing them', async () => { + const userPluginRegisterDebug = jest.fn(); + const userPlugin = { + getMetadata: () => ({ name: 'user-plugin' }), + register: jest.fn(), + registerDebug: userPluginRegisterDebug, + }; + + const { client, testData } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'bool-flag': true }, + { plugins: [userPlugin] }, + ); + await client.start({ bootstrap: {} }); + + expect(userPluginRegisterDebug).toHaveBeenCalled(); + expect(client.boolVariation('bool-flag', false)).toBe(true); + // testData is still the TestData we returned, not shadowed by the user plugin + expect(testData).toBeInstanceOf(TestData); + + await client.close(); +}); + +it('updates flag values dynamically via testData after the client is started', async () => { + const { client, testData } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'show-banner': true }, + ); + await client.start({ bootstrap: {} }); + + expect(client.boolVariation('show-banner', false)).toBe(true); + + testData.setBool('show-banner', false); + expect(client.boolVariation('show-banner', true)).toBe(false); + + await client.close(); +}); diff --git a/packages/tooling/client-testing-plugin/__tests__/clients/react-sdk.test.ts b/packages/tooling/client-testing-plugin/__tests__/clients/react-sdk.test.ts new file mode 100644 index 0000000000..41e340d402 --- /dev/null +++ b/packages/tooling/client-testing-plugin/__tests__/clients/react-sdk.test.ts @@ -0,0 +1,65 @@ +/** + * @jest-environment jsdom + */ +import { createTestClient, createTestClientProvider } from '../../src/clients/react-sdk'; +import TestData from '../../src/TestData'; + +it('seeds the TestData with initialFlags and resolves overrides after start', async () => { + const { client, testData } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'bool-flag': true, greeting: 'hello' }, + ); + await client.start({ bootstrap: {} }); + + expect(testData).toBeInstanceOf(TestData); + expect(client.boolVariation('bool-flag', false)).toBe(true); + expect(client.stringVariation('greeting', 'default')).toBe('hello'); + + await client.close(); +}); + +it('appends TestData to user-supplied plugins rather than replacing them', async () => { + const userPluginRegisterDebug = jest.fn(); + const userPlugin = { + getMetadata: () => ({ name: 'user-plugin' }), + register: jest.fn(), + registerDebug: userPluginRegisterDebug, + }; + + const { client } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'bool-flag': true }, + { plugins: [userPlugin] }, + ); + await client.start({ bootstrap: {} }); + + expect(userPluginRegisterDebug).toHaveBeenCalled(); + expect(client.boolVariation('bool-flag', false)).toBe(true); + + await client.close(); +}); + +it('createTestClientProvider returns a pre-wired Provider, client, and testData', () => { + const { Provider, client, testData } = createTestClientProvider( + { kind: 'user', key: 'tester' }, + { 'show-banner': true }, + ); + expect(Provider).toBeInstanceOf(Function); + expect(client).toBeDefined(); + expect(testData).toBeInstanceOf(TestData); +}); + +it('updates flag values dynamically via testData after the client is started', async () => { + const { client, testData } = createTestClient( + { kind: 'user', key: 'tester' }, + { 'show-banner': true }, + ); + await client.start({ bootstrap: {} }); + + expect(client.boolVariation('show-banner', false)).toBe(true); + + testData.setBool('show-banner', false); + expect(client.boolVariation('show-banner', true)).toBe(false); + + await client.close(); +}); diff --git a/packages/tooling/client-testing-plugin/package.json b/packages/tooling/client-testing-plugin/package.json index fb764a7e76..f11fba2023 100644 --- a/packages/tooling/client-testing-plugin/package.json +++ b/packages/tooling/client-testing-plugin/package.json @@ -23,6 +23,18 @@ "import": "./dist/index.js", "require": "./dist/index.cjs", "default": "./dist/index.js" + }, + "./js-client-sdk": { + "types": "./dist/clients/js-client-sdk.d.ts", + "import": "./dist/clients/js-client-sdk.js", + "require": "./dist/clients/js-client-sdk.cjs", + "default": "./dist/clients/js-client-sdk.js" + }, + "./react-sdk": { + "types": "./dist/clients/react-sdk.d.ts", + "import": "./dist/clients/react-sdk.js", + "require": "./dist/clients/react-sdk.cjs", + "default": "./dist/clients/react-sdk.js" } }, "files": [ @@ -32,24 +44,40 @@ "clean": "rimraf dist", "build": "tsup", "test": "npx jest --ci", - "lint": "eslint . --ext .ts,.tsx" + "lint": "eslint .", + "lint:fix": "yarn run lint --fix" }, "dependencies": { "@launchdarkly/js-client-sdk-common": "workspace:^" }, + "peerDependencies": { + "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/react-sdk": "workspace:^" + }, + "peerDependenciesMeta": { + "@launchdarkly/js-client-sdk": { + "optional": true + }, + "@launchdarkly/react-sdk": { + "optional": true + } + }, "devDependencies": { + "@eslint/js": "^9.0.0", "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/react-sdk": "workspace:^", "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "eslint": "^8.45.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.6.3", + "eslint": "^9.0.0", + "eslint-import-resolver-typescript": "^4.0.0", + "eslint-plugin-import-x": "^4.0.0", + "eslint-plugin-jest": "^28.0.0", + "globals": "^16.0.0", "jest": "^30.2.0", "jest-environment-jsdom": "^30.0.0", "rimraf": "6.0.1", "ts-jest": "^29.1.1", "tsup": "^8.5.1", - "typescript": "5.1.6" + "typescript": "5.1.6", + "typescript-eslint": "^8.0.0" } } diff --git a/packages/tooling/client-testing-plugin/src/clients/js-client-sdk.ts b/packages/tooling/client-testing-plugin/src/clients/js-client-sdk.ts new file mode 100644 index 0000000000..466130cbdd --- /dev/null +++ b/packages/tooling/client-testing-plugin/src/clients/js-client-sdk.ts @@ -0,0 +1,38 @@ +import type { LDFlagValue } from '@launchdarkly/js-client-sdk-common'; +import type { LDClient, LDContext, LDOptions } from '@launchdarkly/js-client-sdk'; +import { createClient } from '@launchdarkly/js-client-sdk'; + +import TestData from '../TestData'; + +const TEST_CLIENT_SIDE_ID = 'client-testing-plugin'; + +export interface CreateTestClientResult { + client: LDClient; + testData: TestData; +} + +/** + * Creates a `@launchdarkly/js-client-sdk` client wired with the `TestData` + * plugin and the settings required for offline test usage. + * + * @param context the LDContext to identify the test client as + * @param initialFlags optional seed map of flag keys to values + * @param options optional LDOptions + * + * @returns an object containing the test client and test data + */ +export function createTestClient( + context: LDContext, + initialFlags?: { [key: string]: LDFlagValue }, + options?: Partial, +): CreateTestClientResult { + const testData = new TestData(initialFlags); + const userPlugins = options?.plugins ?? []; + const client = createClient(TEST_CLIENT_SIDE_ID, context, { + ...options, + plugins: [...userPlugins, testData], + sendEvents: false, + streaming: false, + }); + return { client, testData }; +} diff --git a/packages/tooling/client-testing-plugin/src/clients/react-sdk.ts b/packages/tooling/client-testing-plugin/src/clients/react-sdk.ts new file mode 100644 index 0000000000..96bf88f13b --- /dev/null +++ b/packages/tooling/client-testing-plugin/src/clients/react-sdk.ts @@ -0,0 +1,64 @@ +import type { LDFlagValue } from '@launchdarkly/js-client-sdk-common'; +import type { LDContext, LDReactClient, LDReactClientOptions } from '@launchdarkly/react-sdk'; +import { createClient, createLDReactProviderWithClient } from '@launchdarkly/react-sdk'; + +import TestData from '../TestData'; + +const TEST_CLIENT_SIDE_ID = 'client-testing-plugin'; + +export interface CreateTestClientResult { + client: LDReactClient; + testData: TestData; +} + +export interface CreateTestClientProviderResult { + Provider: ReturnType; + client: LDReactClient; + testData: TestData; +} + +/** + * Creates a `@launchdarkly/react-sdk` client wired with the `TestData` plugin + * and the settings required for offline test usage. + * + * @param context the LDContext to identify the test client as + * @param initialFlags optional seed map of flag keys to values + * @param options optional react-sdk options + * + * @returns an object containing the test client and test data + */ +export function createTestClient( + context: LDContext, + initialFlags?: { [key: string]: LDFlagValue }, + options?: Partial, +): CreateTestClientResult { + const testData = new TestData(initialFlags); + const userPlugins = options?.plugins ?? []; + const client = createClient(TEST_CLIENT_SIDE_ID, context, { + ...options, + plugins: [...userPlugins, testData], + sendEvents: false, + streaming: false, + }); + return { client, testData }; +} + +/** + * Creates a `@launchdarkly/react-sdk` client and a pre-wired Provider component, + * ready to wrap components under test. + * + * @param context the LDContext to identify the test client as + * @param initialFlags optional seed map of flag keys to values + * @param options optional react-sdk options + * + * @returns an object containing the Provider component, client, and testData + */ +export function createTestClientProvider( + context: LDContext, + initialFlags?: { [key: string]: LDFlagValue }, + options?: Partial, +): CreateTestClientProviderResult { + const { client, testData } = createTestClient(context, initialFlags, options); + const Provider = createLDReactProviderWithClient(client); + return { Provider, client, testData }; +} diff --git a/packages/tooling/client-testing-plugin/tsup.config.js b/packages/tooling/client-testing-plugin/tsup.config.js index a262ad37b8..292c9ba67d 100644 --- a/packages/tooling/client-testing-plugin/tsup.config.js +++ b/packages/tooling/client-testing-plugin/tsup.config.js @@ -2,7 +2,11 @@ import { defineConfig } from 'tsup'; export default defineConfig([ { - entry: { index: 'src/index.ts' }, + entry: { + 'index': 'src/index.ts', + 'clients/js-client-sdk': 'src/clients/js-client-sdk.ts', + 'clients/react-sdk': 'src/clients/react-sdk.ts', + }, format: ['esm', 'cjs'], dts: true, clean: true,