diff --git a/.github/workflows/wokwi-test.yml b/.github/workflows/wokwi-test.yml new file mode 100644 index 0000000000..9d16bfc4ed --- /dev/null +++ b/.github/workflows/wokwi-test.yml @@ -0,0 +1,155 @@ +name: Wokwi ESP32 Simulation Test + +on: + push: + branches: [ "mdev", "copilot/**" ] + pull_request: + branches: [ "mdev" ] + workflow_dispatch: + +jobs: + wokwi-test: + name: Test WLED with Wokwi Simulator + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-pio-esp32dev_compat + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install PlatformIO + run: pip install -r requirements.txt + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Node.js dependencies + run: npm ci + + - name: Build web UI + run: npm run build + + - name: Build firmware for ESP32 + env: + WLED_RELEASE: True + run: pio run -e esp32dev_compat + + - name: Install Wokwi CLI + run: curl -L https://wokwi.com/ci/install.sh | sh + + - name: Prepare firmware for Wokwi + run: ./test/wokwi/prepare-firmware.sh esp32dev_compat + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Debug - Verify token is set + run: | + if [ -z "$WOKWI_CLI_TOKEN" ]; then + echo "❌ ERROR: WOKWI_CLI_TOKEN is not set" + echo "Please configure WOKWI_CLI_TOKEN as a repository secret" + exit 1 + else + echo "✅ WOKWI_CLI_TOKEN is set (length: ${#WOKWI_CLI_TOKEN} characters)" + fi + env: + WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} + + - name: Start Wokwi simulator in background + env: + WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} + run: | + cd test/wokwi + # Create log directory + mkdir -p logs + + # Export the token so it's available to child processes + export WOKWI_CLI_TOKEN + + # Start simulator in background with a 180 second timeout + WOKWI_TIMEOUT=180 ./run-simulator.sh > logs/wokwi.log 2>&1 & + WOKWI_PID=$! + echo "WOKWI_PID=$WOKWI_PID" >> $GITHUB_ENV + echo "Started Wokwi simulator with PID $WOKWI_PID" + + # Wait for simulator to start and web server to be ready + echo "Waiting for WLED web server to be ready..." + max_wait=120 + elapsed=0 + while [ $elapsed -lt $max_wait ]; do + if curl -s -f http://localhost:8080 > /dev/null 2>&1; then + echo "Web server is ready after $elapsed seconds!" + break + fi + if ! kill -0 $WOKWI_PID 2>/dev/null; then + echo "Error: Wokwi simulator process died" + echo "Last 50 lines of Wokwi log:" + tail -50 logs/wokwi.log || true + exit 1 + fi + echo "Still waiting... ($elapsed seconds)" + sleep 5 + elapsed=$((elapsed + 5)) + done + + if [ $elapsed -ge $max_wait ]; then + echo "Error: Web server did not start within $max_wait seconds" + echo "Last 50 lines of Wokwi log:" + tail -50 logs/wokwi.log || true + kill $WOKWI_PID || true + exit 1 + fi + + echo "WLED is ready for testing!" + + - name: Run Playwright tests + run: npm run test:wokwi + env: + CI: true + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Stop Wokwi simulator + if: always() + run: | + if [ ! -z "$WOKWI_PID" ]; then + kill $WOKWI_PID || true + fi + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: wokwi-test-results + path: | + test/wokwi/logs/ + test-results/ + playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index c3e06ea53b..4d7cc8f438 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,15 @@ compile_commands.json /wled00/wled00.ino.cpp /wled00/html_*.h _codeql_detected_source_root + +# Playwright and test artifacts +/test-results/ +/playwright-report/ +/playwright/.cache/ + +# Wokwi runtime files +/test/wokwi/firmware.bin +/test/wokwi/firmware.elf +/test/wokwi/.wokwi/ +/test/wokwi/logs/ + diff --git a/package-lock.json b/package-lock.json index 1b5e268712..b731bbdbd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,25 @@ "inliner": "^1.13.1", "nodemon": "^2.0.20", "zlib": "^1.0.5" + }, + "devDependencies": { + "@playwright/test": "^1.40.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/abbrev": { @@ -1507,6 +1526,38 @@ "node": ">=0.10.0" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -2172,6 +2223,15 @@ } }, "dependencies": { + "@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "requires": { + "playwright": "1.57.0" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3384,6 +3444,22 @@ "pinkie": "^2.0.0" } }, + "playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.57.0" + } + }, + "playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", diff --git a/package.json b/package.json index 953590963d..b3001ab767 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ }, "scripts": { "build": "node tools/cdata.js", - "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js" + "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js", + "test": "npm run test:cdata", + "test:cdata": "node tools/cdata-test.js", + "test:wokwi": "playwright test test/playwright/wokwi-basic.spec.js" }, "repository": { "type": "git", @@ -27,5 +30,8 @@ "inliner": "^1.13.1", "nodemon": "^2.0.20", "zlib": "^1.0.5" + }, + "devDependencies": { + "@playwright/test": "^1.40.0" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..6e2867b563 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,52 @@ +const { defineConfig, devices } = require('@playwright/test'); + +/** + * Playwright configuration for WLED-MM Wokwi testing + * See https://playwright.dev/docs/test-configuration. + */ +module.exports = defineConfig({ + testDir: './test/playwright', + + /* Run tests in files in parallel */ + fullyParallel: false, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + + /* Opt out of parallel tests on CI. */ + workers: 1, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Screenshot on failure */ + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'echo "Wokwi simulator should be started separately"', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/test/playwright/wokwi-basic.spec.js b/test/playwright/wokwi-basic.spec.js new file mode 100644 index 0000000000..4466afe899 --- /dev/null +++ b/test/playwright/wokwi-basic.spec.js @@ -0,0 +1,136 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Basic WLED-MM Web Interface Tests + * These tests verify that the web interface loads correctly + * and doesn't have JavaScript errors on basic pages. + */ + +test.describe('WLED-MM Basic Web Interface', () => { + let consoleErrors = []; + let pageErrors = []; + + test.beforeEach(async ({ page }) => { + // Reset error collectors + consoleErrors = []; + pageErrors = []; + + // Listen for console errors + page.on('console', msg => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + // Listen for page errors + page.on('pageerror', error => { + pageErrors.push(error.message); + }); + }); + + test('should load main index page without errors', async ({ page }) => { + await page.goto('/'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Check for page title or main content + const title = await page.title(); + expect(title).toBeTruthy(); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('Main page loaded successfully'); + }); + + test('should load settings page without errors', async ({ page }) => { + await page.goto('/settings.htm'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('Settings page loaded successfully'); + }); + + test('should load WiFi settings page without errors', async ({ page }) => { + await page.goto('/settings/wifi'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('WiFi settings page loaded successfully'); + }); + + test('should load LED settings page without errors', async ({ page }) => { + await page.goto('/settings/leds'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('LED settings page loaded successfully'); + }); + + test('should load UI settings page without errors', async ({ page }) => { + await page.goto('/settings/ui'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('UI settings page loaded successfully'); + }); + + test('should load edit page without errors', async ({ page }) => { + await page.goto('/edit.htm'); + + // Wait for page to be loaded + await page.waitForLoadState('networkidle'); + + // Verify no JavaScript errors occurred + expect(consoleErrors).toHaveLength(0); + expect(pageErrors).toHaveLength(0); + + console.log('Edit page loaded successfully'); + }); + + test('should be able to check JSON API info', async ({ page }) => { + const response = await page.goto('/json/info'); + + expect(response?.status()).toBe(200); + + const json = await response?.json(); + expect(json).toBeTruthy(); + expect(json.ver).toBeTruthy(); // Should have version + + console.log('JSON API responding correctly, version:', json.ver); + }); + + test('should be able to check JSON API state', async ({ page }) => { + const response = await page.goto('/json/state'); + + expect(response?.status()).toBe(200); + + const json = await response?.json(); + expect(json).toBeTruthy(); + expect(json.on).toBeDefined(); // Should have on/off state + + console.log('JSON state API responding correctly'); + }); +}); diff --git a/test/wokwi/README.md b/test/wokwi/README.md new file mode 100644 index 0000000000..3750dbc371 --- /dev/null +++ b/test/wokwi/README.md @@ -0,0 +1,110 @@ +# WLED-MM Wokwi Simulation Testing + +This directory contains configuration and tests for running WLED-MM in the Wokwi ESP32 simulator with Playwright-based web interface testing. + +## Overview + +The Wokwi testing workflow: +1. Builds the WLED firmware for ESP32 +2. Runs the firmware in the Wokwi ESP32 simulator +3. Uses Playwright to test the web interface +4. Verifies pages load without JavaScript errors + +## Files + +- `diagram.json` - Wokwi hardware configuration (ESP32 DevKit) +- `wokwi.toml` - Wokwi CLI configuration and port forwarding +- `prepare-firmware.sh` - Script to copy built firmware to test directory +- `run-simulator.sh` - Script to start the Wokwi simulator + +## Running Tests Locally + +### Prerequisites + +1. Install Node.js dependencies: + ```bash + npm ci + ``` + +2. Install Wokwi CLI: + ```bash + curl -L https://wokwi.com/ci/install.sh | sh + ``` + +3. Install Playwright browsers: + ```bash + npx playwright install --with-deps chromium + ``` + +### Build and Test + +1. Build the web interface: + ```bash + npm run build + ``` + +2. Build the firmware: + ```bash + pio run -e esp32dev_compat + ``` + +3. Prepare firmware for testing: + ```bash + ./test/wokwi/prepare-firmware.sh esp32dev_compat + ``` + +4. Start the Wokwi simulator (in a separate terminal): + ```bash + cd test/wokwi + ./run-simulator.sh + ``` + +5. Run Playwright tests (in another terminal): + ```bash + npm run test:wokwi + ``` + +## CI Integration + +The GitHub Actions workflow (`.github/workflows/wokwi-test.yml`) automatically runs these tests on: +- Push to `mdev` branch +- Pull requests to `mdev` branch +- Manual workflow dispatch + +## Test Cases + +The Playwright tests (`test/playwright/wokwi-basic.spec.js`) verify: +- Main index page loads without errors +- Settings pages load without errors +- Edit page loads without errors +- JSON API endpoints respond correctly + +## Extending Tests + +To add more tests: +1. Edit `test/playwright/wokwi-basic.spec.js` +2. Add new test cases using Playwright's `test()` function +3. Follow the existing pattern of checking for console errors + +## Troubleshooting + +### Simulator doesn't start +- Check that firmware.bin exists in test/wokwi/ +- Verify Wokwi CLI is installed: `wokwi-cli --version` +- Check Wokwi CLI logs for errors + +### Web server not accessible +- Wait 30-60 seconds for the ESP32 to boot and start WiFi +- Check that port 8080 is not already in use +- Verify port forwarding in wokwi.toml + +### Tests fail +- Check Playwright report: `npx playwright show-report` +- Look for console errors in test output +- Verify firmware build completed successfully + +## References + +- [Wokwi Documentation](https://docs.wokwi.com/) +- [Wokwi CLI](https://docs.wokwi.com/wokwi-ci/getting-started) +- [Playwright Documentation](https://playwright.dev/) diff --git a/test/wokwi/diagram.json b/test/wokwi/diagram.json new file mode 100644 index 0000000000..51c1520f36 --- /dev/null +++ b/test/wokwi/diagram.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "author": "WLED-MM CI", + "editor": "wokwi", + "parts": [ + { + "type": "wokwi-esp32-devkit-v1", + "id": "esp", + "top": 0, + "left": 0, + "attrs": {} + } + ], + "connections": [], + "dependencies": {} +} diff --git a/test/wokwi/prepare-firmware.sh b/test/wokwi/prepare-firmware.sh new file mode 100755 index 0000000000..171d0df463 --- /dev/null +++ b/test/wokwi/prepare-firmware.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Script to prepare firmware for Wokwi testing +# This copies the built firmware to the test directory + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +WOKWI_DIR="$PROJECT_ROOT/test/wokwi" + +# Check if environment is specified +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "Example: $0 esp32dev_compat" + exit 1 +fi + +ENV_NAME=$1 +FIRMWARE_BIN="$PROJECT_ROOT/.pio/build/$ENV_NAME/firmware.bin" +FIRMWARE_ELF="$PROJECT_ROOT/.pio/build/$ENV_NAME/firmware.elf" + +# Check if firmware exists +if [ ! -f "$FIRMWARE_BIN" ]; then + echo "Error: Firmware binary not found at $FIRMWARE_BIN" + echo "Please build the firmware first: pio run -e $ENV_NAME" + exit 1 +fi + +# Copy firmware to test directory +echo "Copying firmware from $ENV_NAME to test directory..." +cp "$FIRMWARE_BIN" "$WOKWI_DIR/firmware.bin" + +if [ -f "$FIRMWARE_ELF" ]; then + cp "$FIRMWARE_ELF" "$WOKWI_DIR/firmware.elf" + echo "Copied firmware.bin and firmware.elf" +else + echo "Warning: firmware.elf not found, copying only firmware.bin" +fi + +echo "Firmware prepared successfully!" +echo "Location: $WOKWI_DIR/firmware.bin" diff --git a/test/wokwi/run-simulator.sh b/test/wokwi/run-simulator.sh new file mode 100755 index 0000000000..df27495030 --- /dev/null +++ b/test/wokwi/run-simulator.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Script to run Wokwi simulator with the built firmware +# This script starts the Wokwi CLI simulator and waits for it to be ready + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WOKWI_TIMEOUT=${WOKWI_TIMEOUT:-120} + +cd "$SCRIPT_DIR" + +# Check if firmware exists +if [ ! -f "firmware.bin" ]; then + echo "Error: firmware.bin not found in $SCRIPT_DIR" + echo "Please run prepare-firmware.sh first" + exit 1 +fi + +echo "Starting Wokwi simulator..." +echo "Timeout: ${WOKWI_TIMEOUT} seconds" +echo "Web server will be available at http://localhost:8080" + +# Run wokwi-cli with timeout (in milliseconds) +# The simulator will forward port 80 to localhost:8080 +# Note: wokwi-cli runs in foreground, so this needs to be backgrounded or run in a separate process +# When run from the directory containing diagram.json and wokwi.toml, wokwi-cli will find them automatically +wokwi-cli --timeout ${WOKWI_TIMEOUT}000 . diff --git a/test/wokwi/wokwi.toml b/test/wokwi/wokwi.toml new file mode 100644 index 0000000000..cf7922d5db --- /dev/null +++ b/test/wokwi/wokwi.toml @@ -0,0 +1,9 @@ +[wokwi] +version = 1 +firmware = "firmware.bin" +elf = "firmware.elf" + +[[net.forward]] +# Forward the web server port +from = "localhost:8080" +to = "target:80" diff --git a/tools/cdata-test.js b/tools/cdata-test.js new file mode 100644 index 0000000000..29391a41b2 --- /dev/null +++ b/tools/cdata-test.js @@ -0,0 +1,35 @@ +/** + * Simple test to verify cdata.js build process works + */ + +const fs = require("fs"); +const path = require("path"); + +console.log("Running cdata.js build test..."); + +// Check if required files exist +const requiredFiles = [ + "wled00/data/index.htm", + "wled00/data/settings.htm", + "wled00/html_ui.h" +]; + +let allFilesExist = true; + +for (const file of requiredFiles) { + const filePath = path.join(__dirname, "..", file); + if (!fs.existsSync(filePath)) { + console.error(`❌ Required file not found: ${file}`); + allFilesExist = false; + } else { + console.log(`✓ Found: ${file}`); + } +} + +if (!allFilesExist) { + console.error("\n❌ Some required files are missing. Please run 'npm run build' first."); + process.exit(1); +} + +console.log("\n✓ All tests passed!"); +process.exit(0);