From a6948c65ebd6e08721917d676782bfc9f174e9e1 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:20:18 -0800 Subject: [PATCH 1/5] chore: Run browser contract tests in CI. --- .github/workflows/browser-contract-tests.yml | 124 ++++++++++++++++++ .../browser/contract-tests/open-browser.mjs | 42 ++++++ 2 files changed, 166 insertions(+) create mode 100644 .github/workflows/browser-contract-tests.yml create mode 100644 packages/sdk/browser/contract-tests/open-browser.mjs diff --git a/.github/workflows/browser-contract-tests.yml b/.github/workflows/browser-contract-tests.yml new file mode 100644 index 0000000000..c3cd8507a8 --- /dev/null +++ b/.github/workflows/browser-contract-tests.yml @@ -0,0 +1,124 @@ +name: Browser SDK Contract Tests + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' + pull_request: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' + +jobs: + browser-contract-tests: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18, 22] + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + + - id: shared + name: Shared CI Steps + uses: ./actions/ci + with: + workspace_name: '@launchdarkly/js-client-sdk' + workspace_path: packages/sdk/browser + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Install contract test dependencies + run: | + yarn workspace browser-contract-test-adapter install --no-immutable + yarn workspace browser-contract-test-service install --no-immutable + + - name: Build contract test adapter + run: yarn workspace browser-contract-test-adapter run build + + - name: Build contract test entity (browser app) + run: yarn workspace browser-contract-test-service run build + + - name: Start contract test adapter in background + run: | + yarn workspace browser-contract-test-adapter run start > /tmp/adapter.log 2>&1 & + echo $! > /tmp/adapter.pid + + - name: Serve browser app with http-server + run: | + npx http-server packages/sdk/browser/contract-tests/entity/dist -p 5173 --cors > /tmp/http-server.log 2>&1 & + echo $! > /tmp/http-server.pid + + - name: Wait for services to be ready + run: | + echo "Waiting for adapter on port 8001..." + for i in {1..30}; do + if nc -z localhost 8001; then + echo "Adapter WebSocket ready" + break + fi + if [ $i -eq 30 ]; then + echo "Timeout waiting for adapter" + cat /tmp/adapter.log + exit 1 + fi + sleep 1 + done + + echo "Waiting for HTTP server on port 5173..." + for i in {1..30}; do + if curl -s http://localhost:5173 > /dev/null; then + echo "HTTP server ready" + break + fi + if [ $i -eq 30 ]; then + echo "Timeout waiting for HTTP server" + cat /tmp/http-server.log + exit 1 + fi + sleep 1 + done + + - name: Open browser app in headless Chromium + run: | + node packages/sdk/browser/contract-tests/open-browser.mjs http://localhost:5173 > /tmp/playwright.log 2>&1 & + echo $! > /tmp/playwright.pid + sleep 5 # Give the browser time to initialize and connect via WebSocket + + - name: Run contract tests + run: | + mkdir -p /tmp/sdk-test-harness + git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness + cd /tmp/sdk-test-harness + go build -o test-harness . + ./test-harness -url http://localhost:8000 -debug -stop-service-at-end --skip-from=$GITHUB_WORKSPACE/packages/sdk/browser/contract-tests/suppressions.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Print logs on failure + if: failure() + run: | + echo "=== Adapter Log ===" + cat /tmp/adapter.log || echo "No adapter log" + echo "=== HTTP Server Log ===" + cat /tmp/http-server.log || echo "No http-server log" + echo "=== Playwright Log ===" + cat /tmp/playwright.log || echo "No playwright log" + + - name: Cleanup + if: always() + run: | + [ -f /tmp/playwright.pid ] && kill $(cat /tmp/playwright.pid) || true + [ -f /tmp/http-server.pid ] && kill $(cat /tmp/http-server.pid) || true + [ -f /tmp/adapter.pid ] && kill $(cat /tmp/adapter.pid) || true + pkill -f "playwright" || true + pkill -f "http-server" || true + pkill -f "browser-contract-test-adapter" || true diff --git a/packages/sdk/browser/contract-tests/open-browser.mjs b/packages/sdk/browser/contract-tests/open-browser.mjs new file mode 100644 index 0000000000..ce63f7f843 --- /dev/null +++ b/packages/sdk/browser/contract-tests/open-browser.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +/** + * 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:5173 + */ + +import { chromium } from 'playwright'; + +const url = process.argv[2] || 'http://localhost:5173'; + +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 +}); From 9f1231945e17b7cedee9b488c3d7b26362d755ef Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:28:54 -0800 Subject: [PATCH 2/5] Combine workflows. Add readme. --- .github/workflows/browser-contract-tests.yml | 124 ------------------ .github/workflows/browser.yml | 91 +++++++++++++ packages/sdk/browser/contract-tests/README.md | 53 ++++++++ 3 files changed, 144 insertions(+), 124 deletions(-) delete mode 100644 .github/workflows/browser-contract-tests.yml create mode 100644 packages/sdk/browser/contract-tests/README.md diff --git a/.github/workflows/browser-contract-tests.yml b/.github/workflows/browser-contract-tests.yml deleted file mode 100644 index c3cd8507a8..0000000000 --- a/.github/workflows/browser-contract-tests.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: Browser SDK Contract Tests - -on: - push: - branches: [main, 'feat/**'] - paths-ignore: - - '**.md' - pull_request: - branches: [main, 'feat/**'] - paths-ignore: - - '**.md' - -jobs: - browser-contract-tests: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18, 22] - - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - - - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 - with: - node-version: ${{ matrix.node-version }} - registry-url: 'https://registry.npmjs.org' - - - id: shared - name: Shared CI Steps - uses: ./actions/ci - with: - workspace_name: '@launchdarkly/js-client-sdk' - workspace_path: packages/sdk/browser - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - - name: Install contract test dependencies - run: | - yarn workspace browser-contract-test-adapter install --no-immutable - yarn workspace browser-contract-test-service install --no-immutable - - - name: Build contract test adapter - run: yarn workspace browser-contract-test-adapter run build - - - name: Build contract test entity (browser app) - run: yarn workspace browser-contract-test-service run build - - - name: Start contract test adapter in background - run: | - yarn workspace browser-contract-test-adapter run start > /tmp/adapter.log 2>&1 & - echo $! > /tmp/adapter.pid - - - name: Serve browser app with http-server - run: | - npx http-server packages/sdk/browser/contract-tests/entity/dist -p 5173 --cors > /tmp/http-server.log 2>&1 & - echo $! > /tmp/http-server.pid - - - name: Wait for services to be ready - run: | - echo "Waiting for adapter on port 8001..." - for i in {1..30}; do - if nc -z localhost 8001; then - echo "Adapter WebSocket ready" - break - fi - if [ $i -eq 30 ]; then - echo "Timeout waiting for adapter" - cat /tmp/adapter.log - exit 1 - fi - sleep 1 - done - - echo "Waiting for HTTP server on port 5173..." - for i in {1..30}; do - if curl -s http://localhost:5173 > /dev/null; then - echo "HTTP server ready" - break - fi - if [ $i -eq 30 ]; then - echo "Timeout waiting for HTTP server" - cat /tmp/http-server.log - exit 1 - fi - sleep 1 - done - - - name: Open browser app in headless Chromium - run: | - node packages/sdk/browser/contract-tests/open-browser.mjs http://localhost:5173 > /tmp/playwright.log 2>&1 & - echo $! > /tmp/playwright.pid - sleep 5 # Give the browser time to initialize and connect via WebSocket - - - name: Run contract tests - run: | - mkdir -p /tmp/sdk-test-harness - git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness - cd /tmp/sdk-test-harness - go build -o test-harness . - ./test-harness -url http://localhost:8000 -debug -stop-service-at-end --skip-from=$GITHUB_WORKSPACE/packages/sdk/browser/contract-tests/suppressions.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Print logs on failure - if: failure() - run: | - echo "=== Adapter Log ===" - cat /tmp/adapter.log || echo "No adapter log" - echo "=== HTTP Server Log ===" - cat /tmp/http-server.log || echo "No http-server log" - echo "=== Playwright Log ===" - cat /tmp/playwright.log || echo "No playwright log" - - - name: Cleanup - if: always() - run: | - [ -f /tmp/playwright.pid ] && kill $(cat /tmp/playwright.pid) || true - [ -f /tmp/http-server.pid ] && kill $(cat /tmp/http-server.pid) || true - [ -f /tmp/adapter.pid ] && kill $(cat /tmp/adapter.pid) || true - pkill -f "playwright" || true - pkill -f "http-server" || true - pkill -f "browser-contract-test-adapter" || true diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index ca33545f13..f9e1c33120 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -42,3 +42,94 @@ jobs: package_name: '@launchdarkly/js-client-sdk' pr_number: ${{ github.event.number }} size_limit: 25000 + + # Contract Tests + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Install contract test dependencies + run: | + yarn workspace browser-contract-test-adapter install --no-immutable + yarn workspace browser-contract-test-service install --no-immutable + + - name: Build contract test adapter + run: yarn workspace browser-contract-test-adapter run build + + - name: Build contract test entity (browser app) + run: yarn workspace browser-contract-test-service run build + + - name: Start contract test adapter in background + run: | + yarn workspace browser-contract-test-adapter run start > /tmp/adapter.log 2>&1 & + echo $! > /tmp/adapter.pid + + - name: Serve browser app with http-server + run: | + npx http-server packages/sdk/browser/contract-tests/entity/dist -p 5173 --cors > /tmp/http-server.log 2>&1 & + echo $! > /tmp/http-server.pid + + - name: Wait for services to be ready + run: | + echo "Waiting for adapter on port 8001..." + for i in {1..30}; do + if nc -z localhost 8001; then + echo "Adapter WebSocket ready" + break + fi + if [ $i -eq 30 ]; then + echo "Timeout waiting for adapter" + cat /tmp/adapter.log + exit 1 + fi + sleep 1 + done + + echo "Waiting for HTTP server on port 5173..." + for i in {1..30}; do + if curl -s http://localhost:5173 > /dev/null; then + echo "HTTP server ready" + break + fi + if [ $i -eq 30 ]; then + echo "Timeout waiting for HTTP server" + cat /tmp/http-server.log + exit 1 + fi + sleep 1 + done + + - name: Open browser app in headless Chromium + run: | + node packages/sdk/browser/contract-tests/open-browser.mjs http://localhost:5173 > /tmp/playwright.log 2>&1 & + echo $! > /tmp/playwright.pid + sleep 5 # Give the browser time to initialize and connect via WebSocket + + - name: Run contract tests + run: | + mkdir -p /tmp/sdk-test-harness + git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness + cd /tmp/sdk-test-harness + go build -o test-harness . + ./test-harness -url http://localhost:8000 -debug -stop-service-at-end --skip-from=$GITHUB_WORKSPACE/packages/sdk/browser/contract-tests/suppressions.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Print logs on failure + if: failure() + run: | + echo "=== Adapter Log ===" + cat /tmp/adapter.log || echo "No adapter log" + echo "=== HTTP Server Log ===" + cat /tmp/http-server.log || echo "No http-server log" + echo "=== Playwright Log ===" + cat /tmp/playwright.log || echo "No playwright log" + + - name: Cleanup contract test services + if: always() + run: | + [ -f /tmp/playwright.pid ] && kill $(cat /tmp/playwright.pid) || true + [ -f /tmp/http-server.pid ] && kill $(cat /tmp/http-server.pid) || true + [ -f /tmp/adapter.pid ] && kill $(cat /tmp/adapter.pid) || true + pkill -f "playwright" || true + pkill -f "http-server" || true + pkill -f "browser-contract-test-adapter" || true diff --git a/packages/sdk/browser/contract-tests/README.md b/packages/sdk/browser/contract-tests/README.md new file mode 100644 index 0000000000..73397575c2 --- /dev/null +++ b/packages/sdk/browser/contract-tests/README.md @@ -0,0 +1,53 @@ +# Browser SDK Contract Tests + +This directory contains the contract test implementation for the LaunchDarkly Browser SDK using the [SDK Test Harness](https://github.com/launchdarkly/sdk-test-harness). + +## Architecture + +The browser contract tests consist of three components: + +1. **Adapter** (`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** (`entity/`): A browser application (Vite app) that: + - Connects to the adapter via WebSocket + - Implements the actual SDK test logic + - Runs the Browser SDK in a real browser environment + +3. **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 +# From the repository root +./packages/sdk/browser/contract-tests/run-test-service.sh +``` + +This script will: +1. Start the adapter (WebSocket bridge) +2. Start the entity (browser app with Vite dev server) +3. Open the browser app in your default browser + +The services will be available at: +- Adapter REST API: http://localhost:8000 +- Adapter WebSocket: ws://localhost:8001 +- Browser App: http://localhost:5173 + +You then run the `sdk-test-harness`. More information is available here: https://github.com/launchdarkly/sdk-test-harness + +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 +``` From af08f94e97355cca552bf5881cc915b1c0900493 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:41:04 -0800 Subject: [PATCH 3/5] Run tests using shared action. --- .github/workflows/browser.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index f9e1c33120..135e56aa03 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -48,9 +48,7 @@ jobs: run: npx playwright install --with-deps chromium - name: Install contract test dependencies - run: | - yarn workspace browser-contract-test-adapter install --no-immutable - yarn workspace browser-contract-test-service install --no-immutable + run: yarn workspaces focus browser-contract-test-adapter browser-contract-test-service - name: Build contract test adapter run: yarn workspace browser-contract-test-adapter run build @@ -105,14 +103,11 @@ jobs: sleep 5 # Give the browser time to initialize and connect via WebSocket - name: Run contract tests - run: | - mkdir -p /tmp/sdk-test-harness - git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness - cd /tmp/sdk-test-harness - go build -o test-harness . - ./test-harness -url http://localhost:8000 -debug -stop-service-at-end --skip-from=$GITHUB_WORKSPACE/packages/sdk/browser/contract-tests/suppressions.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: launchdarkly/gh-actions/actions/contract-tests@21174f3a7f3aa3e3121227ec91842e8a1ebeec6e + with: + test_service_port: 8000 + token: ${{ secrets.GITHUB_TOKEN }} + extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions.txt --stop-service-at-end' - name: Print logs on failure if: failure() From e035f910ecd8a138c68e7128946959ff88d3a244 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:57:04 -0800 Subject: [PATCH 4/5] Playwright tweaks. --- .github/workflows/browser.yml | 2 +- .../sdk/browser/contract-tests/{ => entity}/open-browser.mjs | 0 packages/sdk/browser/contract-tests/entity/package.json | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename packages/sdk/browser/contract-tests/{ => entity}/open-browser.mjs (100%) diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index 135e56aa03..3abfe95586 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -98,7 +98,7 @@ jobs: - name: Open browser app in headless Chromium run: | - node packages/sdk/browser/contract-tests/open-browser.mjs http://localhost:5173 > /tmp/playwright.log 2>&1 & + node packages/sdk/browser/contract-tests/entity/open-browser.mjs http://localhost:5173 > /tmp/playwright.log 2>&1 & echo $! > /tmp/playwright.pid sleep 5 # Give the browser time to initialize and connect via WebSocket diff --git a/packages/sdk/browser/contract-tests/open-browser.mjs b/packages/sdk/browser/contract-tests/entity/open-browser.mjs similarity index 100% rename from packages/sdk/browser/contract-tests/open-browser.mjs rename to packages/sdk/browser/contract-tests/entity/open-browser.mjs diff --git a/packages/sdk/browser/contract-tests/entity/package.json b/packages/sdk/browser/contract-tests/entity/package.json index 9d359fb812..fd02c2de23 100644 --- a/packages/sdk/browser/contract-tests/entity/package.json +++ b/packages/sdk/browser/contract-tests/entity/package.json @@ -24,6 +24,7 @@ "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.5.3", "vite": "^5.4.1" From f7d755b252f8ec261395fe0e0fe0a8e3253e1f29 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:32:40 -0800 Subject: [PATCH 5/5] Try installing from package script. --- .github/workflows/browser.yml | 6 +++--- packages/sdk/browser/contract-tests/entity/package.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index 3abfe95586..9556add47e 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -44,12 +44,12 @@ jobs: size_limit: 25000 # Contract Tests - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - name: Install contract test dependencies run: yarn workspaces focus browser-contract-test-adapter browser-contract-test-service + - name: Install Playwright browsers + run: yarn workspace browser-contract-test-service install-playwright-browsers + - name: Build contract test adapter run: yarn workspace browser-contract-test-adapter run build diff --git a/packages/sdk/browser/contract-tests/entity/package.json b/packages/sdk/browser/contract-tests/entity/package.json index fd02c2de23..6994e63bb1 100644 --- a/packages/sdk/browser/contract-tests/entity/package.json +++ b/packages/sdk/browser/contract-tests/entity/package.json @@ -5,6 +5,7 @@ "type": "module", "description": "Contract test service implementation for @launchdarkly/js-client-sdk", "scripts": { + "install-playwright-browsers": "playwright install --with-deps chromium", "start": "tsc --noEmit && vite --open=true", "build": "tsc --noEmit && vite build", "lint": "eslint ./src",