Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
'**/vercel/examples/**',
'**/react-native/example/**',
'**/react-universal/example/**',
'**/react-universal/contract-tests/**',
'**/fromExternal/**',
],
rules: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"packages/sdk/react-native/example",
"packages/sdk/react-universal",
"packages/sdk/react-universal/example",
"packages/sdk/react-universal/contract-tests",
"packages/sdk/vercel",
"packages/sdk/svelte",
"packages/sdk/svelte/example",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/browser/contract-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ You then run the `sdk-test-harness`. More information is available here: https:/

Example with local clone of the test harness:
```bash
go run . --url http://localhost:8123 -skip-from path-to-your-js-core-clone/packages/sdk/browser/contract-tests/suppressions.txt
go run . --url http://localhost:8000 -skip-from path-to-your-js-core-clone/packages/sdk/browser/contract-tests/suppressions.txt
```
5 changes: 5 additions & 0 deletions packages/sdk/react-universal/contract-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# next.js
/.next/
/out/

next-env.d.ts
59 changes: 59 additions & 0 deletions packages/sdk/react-universal/contract-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# React SDK contract-tests

This directory contains the contract test implementation for the LaunchDarkly React SDK using the [SDK Test Harness](https://github.com/launchdarkly/sdk-test-harness).

## Architecture
> NOTE: much of the test architecture is based off of
> [browser contract test](../../browser/contract-tests).

This contract test consists of 3 components:

1. [Adapter](../../browser/contract-tests/adapter/): A Node.js server that:
- Exposes a REST API on port 8000 for the test harness
- Runs a WebSocket server on port 8001 for browser communication
- Translates REST commands to WebSocket messages

2. Entity: A browser application (NextJS app) that:
- Connects to the adapter via WebSocket
- Implements the actual SDK test logic
- Runs the React SDK in a real browser environment

3. [Test harness](https://github.com/launchdarkly/sdk-test-harness): The SDK test harness that:
- Sends test commands via REST API to the adapter (port 8000)
- Validates SDK behavior across different scenarios

## Running Locally

### Prerequisites

- Node.js 18 or later
- Yarn
- A modern browser (for manual testing)

### Quick Start

```bash
# Install the workspace if you haven't already
yarn install

# Build contract tests and browser contract test (dependency)
yarn workspaces foreach -pR --topological-dev --from 'browser-contract-test-adapter' run build
yarn workspaces foreach -pR --topological-dev --from '@launchdarkly/react-sdk-contract-tests' run build
```

From the repository root
```bash
./packages/sdk/browser/contract-tests/run-test-service.sh
```

This script will:
1. Start the adapter (WebSocket bridge)
2. Start the app

The services will be available at:
- Adapter REST API: http://localhost:8000
- Adapter WebSocket: ws://localhost:8001
- Browser App: http://localhost:8002

You then run the `sdk-test-harness`. More information is available here: https://github.com/launchdarkly/sdk-test-harness

24 changes: 24 additions & 0 deletions packages/sdk/react-universal/contract-tests/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

stubbed for now


import React, { useEffect } from 'react';

import AdaptorWebSocket from './websocket';

const ws = new AdaptorWebSocket('ws://localhost:8001');

export default function App() {
useEffect(() => {
ws.connect();
return () => {
ws.disconnect();
};
}, []);

return (
<html lang="en">
<body>
<div> Hello test harness </div>
</body>
</html>
);
}
Copy link

Choose a reason for hiding this comment

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

Next.js layout missing required children prop

Medium Severity

The layout.tsx component is missing the required children prop that Next.js App Router layouts must accept and render. Without this prop, any page content added to this app in the future won't be rendered, and the layout won't properly wrap its content. The function signature needs to accept { children } and render it inside the body.

Fix in Cursor Fix in Web

64 changes: 64 additions & 0 deletions packages/sdk/react-universal/contract-tests/app/websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// eslint-disable no-console
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Invalid eslint directive syntax does nothing

Low Severity

The comment // eslint-disable no-console uses incorrect syntax. For file-wide disables, ESLint requires block comment syntax: /* eslint-disable no-console */. The single-line comment format only works with eslint-disable-next-line. While the directory is in .eslintrc.js ignorePatterns making this comment redundant anyway, the incorrect syntax is misleading.

Fix in Cursor Fix in Web


export default class AdaptorWebSocket {
private _ws?: WebSocket;

constructor(private readonly _url: string) {}

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

this._ws.onmessage = async (msg) => {
console.log('Test harness message', msg);
const data = JSON.parse(msg.data);
const resData: any = { reqId: data.reqId };
// TODO: currently copied from the browser contract tests
// will need to figure out what the actual capabilities are.
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':
case 'runCommand':
case 'deleteClient':
default:
break;
}

this.send(resData);
};
}

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

Choose a reason for hiding this comment

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

Disconnect triggers reconnection instead of closing cleanly

Medium Severity

The disconnect() method calls this._ws?.close(), which triggers the onclose handler that unconditionally schedules a reconnection via setTimeout. When the React component unmounts and calls disconnect() during cleanup, this will paradoxically trigger a new connection attempt 1 second later. The class has no mechanism to distinguish intentional disconnection from unexpected connection loss.

Additional Locations (1)

Fix in Cursor Fix in Web


send(data: unknown) {
this._ws?.send(JSON.stringify(data));
}
}
7 changes: 7 additions & 0 deletions packages/sdk/react-universal/contract-tests/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
};

export default nextConfig;
42 changes: 42 additions & 0 deletions packages/sdk/react-universal/contract-tests/open-browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env node
Copy link
Contributor Author

Choose a reason for hiding this comment

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


/**
* Opens a headless browser and navigates to the contract test entity page.
* Keeps the browser open until the process is terminated.
*
* Usage: node open-browser.mjs [url]
* Default URL: http://localhost:8002
*/

import { chromium } from 'playwright';

const url = process.argv[2] || 'http://localhost:8002';

console.log(`Opening headless browser at ${url}...`);

const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});

const context = await browser.newContext();
const page = await context.newPage();

// Log console messages from the browser
page.on('console', (msg) => {
console.log(`[Browser Console] ${msg.type()}: ${msg.text()}`);
});

// Log page errors
page.on('pageerror', (error) => {
console.error(`[Browser Error] ${error.message}`);
});

await page.goto(url);

console.log('Browser is open and running. Press Ctrl+C to close.');

// Keep the process alive
await new Promise(() => {
// Intentionally never resolve - keeps browser open until process is killed
});
34 changes: 34 additions & 0 deletions packages/sdk/react-universal/contract-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@launchdarkly/react-sdk-contract-tests",
"version": "0.0.0",
"private": true,
"packageManager": "yarn@3.4.1",
"scripts": {
"install-playwright-browsers": "playwright install --with-deps chromium",
"start:adapter": "yarn workspace browser-contract-test-adapter run start",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

NOTE: I am using the adapter directly from the browser contract test... I think this is okay, probably should work on extracting this ws bridge to a common private module

"dev": "next dev -p 8002",
"build": "next build",
"start:entity": "next start -p 8002",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
},
"dependencies": {
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"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",
"playwright": "^1.49.1",
"prettier": "^3.0.0",
"typescript": "^5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarn workspace @launchdarkly/react-sdk-contract-tests run start:adapter & yarn workspace @launchdarkly/react-sdk-contract-tests run start:entity && kill $!
34 changes: 34 additions & 0 deletions packages/sdk/react-universal/contract-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
2 changes: 1 addition & 1 deletion packages/sdk/react-universal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"types": ["jest", "node", "react/canary"],
"jsx": "react"
},
"exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"]
"exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example", "contract-tests"]
}