diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index ca33545f13..9556add47e 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -42,3 +42,89 @@ jobs: package_name: '@launchdarkly/js-client-sdk' pr_number: ${{ github.event.number }} size_limit: 25000 + + # Contract Tests + - 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 + + - 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/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 + + - name: Run contract tests + 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() + 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 +``` diff --git a/packages/sdk/browser/contract-tests/entity/open-browser.mjs b/packages/sdk/browser/contract-tests/entity/open-browser.mjs new file mode 100644 index 0000000000..ce63f7f843 --- /dev/null +++ b/packages/sdk/browser/contract-tests/entity/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 +}); diff --git a/packages/sdk/browser/contract-tests/entity/package.json b/packages/sdk/browser/contract-tests/entity/package.json index 9d359fb812..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", @@ -24,6 +25,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"