Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
62adbbd
feat: create shared contract test utilities package (SDK-1866)
devin-ai-integration[bot] Mar 4, 2026
d5fedb9
fix: resolve CI failures - point to source files and fix import paths
devin-ai-integration[bot] Mar 4, 2026
d16c527
fix: split into subpath exports to avoid cross-dependency resolution
devin-ai-integration[bot] Mar 4, 2026
8b8163d
refactor: move contract-test-utils to packages/tooling/ and use local…
devin-ai-integration[bot] Mar 4, 2026
987b0c2
fix: compile shared package to dist/ and build before contract tests
devin-ai-integration[bot] Mar 4, 2026
bb68d02
fix: split exports - source for client/base, compiled for server subpath
devin-ai-integration[bot] Mar 4, 2026
fd38015
fix: inline shared package in shopify-oxygen tsup bundle
devin-ai-integration[bot] Mar 4, 2026
d9bf413
Merge remote-tracking branch 'origin/main' into devin/SDK-1866-177264…
devin-ai-integration[bot] Mar 4, 2026
d04d7ae
feat: migrate electron and react-native contract tests to shared package
devin-ai-integration[bot] Mar 4, 2026
496d545
fix: resolve prettier import ordering in electron and react-native co…
devin-ai-integration[bot] Mar 4, 2026
68929cb
fix: enable package exports in Metro config for subpath import resolu…
devin-ai-integration[bot] Mar 4, 2026
ce6a745
fix: add custom Metro resolver for TypeScript .js extension convention
devin-ai-integration[bot] Mar 4, 2026
5b4b3c1
refactor: extract shared adapter implementation for browser and react…
devin-ai-integration[bot] Mar 5, 2026
961ba7b
ci: re-trigger CI checks
devin-ai-integration[bot] Mar 5, 2026
e79615f
style: consolidate eslint-disable comments in adapter
devin-ai-integration[bot] Mar 5, 2026
35e3007
fix: pre-build shared package server output before adapter compilation
devin-ai-integration[bot] Mar 5, 2026
2f8a2e3
fix: use adapter-only tsconfig to avoid node-server-sdk dependency in CI
devin-ai-integration[bot] Mar 5, 2026
45b270f
fix: switch adapter packages to ESM to resolve ERR_REQUIRE_ESM at run…
devin-ai-integration[bot] Mar 5, 2026
3860a36
feat: add CLI executable (sdk-testharness-server / sts) with config f…
devin-ai-integration[bot] Mar 6, 2026
498810c
docs: add README.md for contract-test-utils package
devin-ai-integration[bot] Mar 6, 2026
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"packages/sdk/shopify-oxygen",
"packages/sdk/shopify-oxygen/contract-tests",
"packages/sdk/shopify-oxygen/example",
"packages/sdk/browser/example"
"packages/sdk/browser/example",
"packages/tooling/contract-test-utils"
],
"private": true,
"scripts": {
Expand Down
32 changes: 3 additions & 29 deletions packages/sdk/browser/contract-tests/adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,13 @@
"name": "browser-contract-test-adapter",
"version": "1.0.0",
"description": "Adapts REST interface to a websocket for use in browsers.",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "yarn build && node dist/index.js",
"lint": "eslint ./src",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
"build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter",
"start": "yarn build && sdk-testharness-server adapter"
},
"author": "",
"license": "UNLICENSED",
"dependencies": {
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"express": "^4.21.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-prettier": "^5.0.0",
"globals": "^15.9.0",
"prettier": "^3.0.0",
"typescript": "^5.6.2",
"typescript-eslint": "^8.5.0"
"@launchdarkly/js-contract-test-utils": "workspace:*"
}
}
15 changes: 0 additions & 15 deletions packages/sdk/browser/contract-tests/adapter/tsconfig.json

This file was deleted.

3 changes: 2 additions & 1 deletion packages/sdk/browser/contract-tests/entity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
},
"dependencies": {
"@launchdarkly/js-client-sdk": "*"
"@launchdarkly/js-client-sdk": "*",
"@launchdarkly/js-contract-test-utils": "workspace:^"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
Expand Down
14 changes: 9 additions & 5 deletions packages/sdk/browser/contract-tests/entity/src/ClientEntity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk';

import { CommandParams, CommandType, ValueType } from './CommandParams';
import { CreateInstanceParams, SDKConfigParams } from './ConfigParams';
import { makeLogger } from './makeLogger';
import TestHook from './TestHook';
import {
CommandParams,
CommandType,
CreateInstanceParams,
makeLogger,
SDKConfigParams,
ClientSideTestHook as TestHook,
ValueType,
} from '@launchdarkly/js-contract-test-utils/client';

export const badCommandError = new Error('unsupported command');
export const malformedCommand = new Error('command was malformed');
Expand Down
90 changes: 0 additions & 90 deletions packages/sdk/browser/contract-tests/entity/src/ConfigParams.ts

This file was deleted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes much sense to have this without the adaptor implementation (eg https://github.com/launchdarkly/js-core/tree/main/packages/sdk/browser/contract-tests/adapter) since the websocket contract is specific to that adaptor implementation. I think it would be good to have a shared adaptor implementation that is executable.

I am already trying to leverage the adaptor implementation from the browser in my react sdk branch (https://github.com/launchdarkly/js-core/tree/skz/react-sdk-next/packages/sdk/react/contract-tests).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — the adapter was indeed duplicated identically between browser and react-native. I've now extracted it into the shared package as @launchdarkly/js-contract-test-utils/adapter (compiled to dist/ since it's Node.js code).

Both browser and react-native adapters are now one-liners:

import { startAdapter } from '@launchdarkly/js-contract-test-utils/adapter';
startAdapter();

The shared startAdapter() function accepts optional { wsPort, httpPort } config (defaults to 8001/8000). This should make it easy to reuse in the react SDK contract tests too.

See commit 5b4b3c1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more effective if we made this an executable and read off modifications from a configuration file (like what jest does for example). Reference https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin and we can call the runner sdk-testharness-server (sts as an alias) and the startAdapter function could be called with the command sdk-testharness-server adapter.

I think the configuration file can be called contract-test.config and we should support different formats (eg json, js, ts, etc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the CLI executable and config file support as suggested:

CLI (sdk-testharness-server / sts):

  • Registered via bin field in the shared package's package.json (npm bin docs)
  • sdk-testharness-server adapter calls startAdapter() with config from the config file
  • sts is registered as an alias

Config file (contract-test.config.*):

  • Supports .json, .js, .mjs, .cjs, .ts, .mts formats (similar to Jest's jest.config.*)
  • Loaded from the current working directory via loadConfig()
  • If no config file is found, defaults are used

Example contract-test.config.json:

{
  "adapter": {
    "wsPort": 8001,
    "httpPort": 8000
  }
}

Adapter packages simplified:
Both browser and react-native adapter packages no longer need their own source code or tsconfig — they just delegate to the CLI:

{
  "scripts": {
    "build": "yarn workspace @launchdarkly/js-contract-test-utils build:adapter",
    "start": "yarn build && sdk-testharness-server adapter"
  }
}

Exported ContractTestConfig type so consumers can type their config files:

import type { ContractTestConfig } from '@launchdarkly/js-contract-test-utils/adapter';

All 43 CI checks pass.

Original file line number Diff line number Diff line change
@@ -1,96 +1,22 @@
import { LDLogger } from '@launchdarkly/js-client-sdk';

import { ClientEntity, newSdkClientEntity } from './ClientEntity';
import { makeLogger } from './makeLogger';

export default class TestHarnessWebSocket {
private _ws?: WebSocket;
private readonly _entities: Record<string, ClientEntity> = {};
private _clientCounter = 0;
private _logger: LDLogger = makeLogger('TestHarnessWebSocket');

constructor(private readonly _url: string) {}

connect() {
this._logger.info(`Connecting to web socket.`);
this._ws = new WebSocket(this._url, ['v1']);
this._ws.onopen = () => {
this._logger.info('Connected to websocket.');
};
this._ws.onclose = () => {
this._logger.info('Websocket closed. Attempting to reconnect in 1 second.');
setTimeout(() => {
this.connect();
}, 1000);
};
this._ws.onerror = (err) => {
this._logger.info(`error:`, err);
};

this._ws.onmessage = async (msg) => {
this._logger.info('Test harness message', msg);
const data = JSON.parse(msg.data);
const resData: any = { reqId: data.reqId };
switch (data.command) {
case 'getCapabilities':
resData.capabilities = [
'client-side',
'service-endpoints',
'tags',
'user-type',
'inline-context-all',
'anonymous-redaction',
'strongly-typed',
'client-prereq-events',
'client-per-context-summaries',
'track-hooks',
];

break;
case 'createClient':
{
resData.resourceUrl = `/clients/${this._clientCounter}`;
resData.status = 201;
const entity = await newSdkClientEntity(data.body);
this._entities[this._clientCounter] = entity;
this._clientCounter += 1;
}
break;
case 'runCommand':
if (Object.prototype.hasOwnProperty.call(this._entities, data.id)) {
const entity = this._entities[data.id];
const body = await entity.doCommand(data.body);
resData.body = body;
resData.status = body ? 200 : 204;
} else {
resData.status = 404;
this._logger.warn(`Client did not exist: ${data.id}`);
}

break;
case 'deleteClient':
if (Object.prototype.hasOwnProperty.call(this._entities, data.id)) {
const entity = this._entities[data.id];
entity.close();
delete this._entities[data.id];
} else {
resData.status = 404;
this._logger.warn(`Could not delete client because it did not exist: ${data.id}`);
}
break;
default:
break;
}

this.send(resData);
};
}

disconnect() {
this._ws?.close();
}

send(data: unknown) {
this._ws?.send(JSON.stringify(data));
import { TestHarnessWebSocket as SharedTestHarnessWebSocket } from '@launchdarkly/js-contract-test-utils/client';

import { newSdkClientEntity } from './ClientEntity';

const CAPABILITIES = [
'client-side',
'service-endpoints',
'tags',
'user-type',
'inline-context-all',
'anonymous-redaction',
'strongly-typed',
'client-prereq-events',
'client-per-context-summaries',
'track-hooks',
];

export default class TestHarnessWebSocket extends SharedTestHarnessWebSocket {
constructor(url: string) {
super(url, CAPABILITIES, newSdkClientEntity);
}
}
1 change: 1 addition & 0 deletions packages/sdk/electron/contract-tests/entity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"vite": "^5.4.21"
},
"dependencies": {
"@launchdarkly/js-contract-test-utils": "workspace:^",
"body-parser": "^2.2.2",
"electron-squirrel-startup": "^1.0.1",
"express": "^5.2.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import path from 'node:path';

// eslint-disable-next-line import/no-extraneous-dependencies
import { createClient, LDClient, LDLogger, LDOptions } from '@launchdarkly/electron-client-sdk';

import { CommandParams, CommandType, ValueType } from './CommandParams';
import { CreateInstanceParams, SDKConfigParams } from './ConfigParams';
import { makeLogger } from './makeLogger';
import TestHook from './TestHook';
import {
CommandParams,
CommandType,
CreateInstanceParams,
makeLogger,
SDKConfigParams,
ValueType,
} from '@launchdarkly/js-contract-test-utils';
import { ClientSideTestHook as TestHook } from '@launchdarkly/js-contract-test-utils/client';

export const badCommandError = new Error('unsupported command');
export const malformedCommand = new Error('command was malformed');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CommandParams, CreateInstanceParams } from '@launchdarkly/js-contract-test-utils';

import { ClientEntity, createEntity } from './ClientEntity';
import { CommandParams } from './CommandParams';
import { CreateInstanceParams } from './ConfigParams';
Comment thread
cursor[bot] marked this conversation as resolved.

export default class ClientFactory {
private _clientCounter = 0;
Expand Down
Loading