diff --git a/.github/workflows/wokwi-test.yml b/.github/workflows/wokwi-test.yml new file mode 100644 index 0000000000..c86ab92d47 --- /dev/null +++ b/.github/workflows/wokwi-test.yml @@ -0,0 +1,250 @@ +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-esp32_V4_wokwi_debug + + - 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 esp32_V4_wokwi_debug + + - name: Install Wokwi CLI + run: | + curl -L https://wokwi.com/ci/install.sh | sh + echo "Wokwi CLI installed to: $HOME/.wokwi-ci/bin/" + ls -la "$HOME/.wokwi-ci/bin/" || echo "Directory not found" + export PATH="$HOME/.wokwi-ci/bin:$PATH" + wokwi-cli --version || echo "Warning: wokwi-cli not accessible" + + - name: Prepare firmware for Wokwi + run: ./test/wokwi/prepare-firmware.sh esp32_V4_wokwi_debug + + - 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: Quick boot validation with scenario + working-directory: test/wokwi + env: + WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} + run: | + # Create log directory + mkdir -p logs + + # Add Wokwi CLI to PATH for this step + export PATH="$HOME/.wokwi-ci/bin:$PATH" + + # Verify firmware files exist + echo "Checking for firmware files..." + if [ ! -f "firmware.bin" ]; then + echo "❌ ERROR: firmware.bin not found in $(pwd)" + echo "Available files:" + ls -la + exit 1 + fi + echo "✅ firmware.bin found ($(du -h firmware.bin | cut -f1))" + + if [ -f "firmware.elf" ]; then + echo "✅ firmware.elf found ($(du -h firmware.elf | cut -f1))" + else + echo "⚠️ firmware.elf not found (optional for simulation)" + fi + + # Verify firmware can be read and has valid ESP32 header + echo "" + echo "Firmware file details:" + ls -lh firmware.bin firmware.elf 2>/dev/null || true + echo "" + echo "Firmware.bin first 64 bytes (hex):" + hexdump -C firmware.bin | head -4 || echo "Cannot read firmware.bin" + echo "Note: ESP32 firmware should start with magic byte 0xe9" + + echo "" + echo "Running quick boot check scenario (15 seconds)..." + echo "Wokwi CLI location: $(which wokwi-cli || echo 'NOT FOUND')" + echo "Wokwi CLI version:" + wokwi-cli --version || echo "Warning: Could not get version" + echo "" + + # Run boot check with increased timeout to account for startup time + # Using 30 second timeout for a 15 second scenario to allow for network delays + # Capture serial output (stdout) and Wokwi CLI diagnostics (stderr) separately + if wokwi-cli --timeout 30000 --scenario scenarios/boot-check.yaml . > logs/boot-check-serial.log 2>logs/boot-check.log; then + echo "✅ Boot check passed - firmware boots without crashes" + echo "" + echo "=== Boot check serial output (last 50 lines) ===" + tail -50 logs/boot-check-serial.log + echo "" + echo "=== Boot check CLI log (last 50 lines) ===" + tail -50 logs/boot-check.log + else + EXIT_CODE=$? + echo "❌ Boot check failed with exit code $EXIT_CODE" + echo "" + echo "=== Boot check CLI log ===" + cat logs/boot-check.log + echo "" + echo "=== Boot check serial output ===" + cat logs/boot-check-serial.log + + # Check if it's a WebSocket connection error (code 1006) + if grep -q "code 1006" logs/boot-check.log || grep -q "Connection.*closed" logs/boot-check.log; then + echo "" + echo "⚠️ WebSocket connection error detected (code 1006)" + echo "This is typically a transient network issue with Wokwi's API." + echo "The workflow will continue with the full simulation test." + echo "If this persists, check Wokwi service status or network connectivity." + # Don't fail - this might be a transient issue + else + # Other errors should fail the build + exit 1 + fi + fi + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - 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 CLI outputs to stderr, serial output goes to stdout + WOKWI_TIMEOUT=300 ./run-simulator.sh >logs/serial.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=240 + 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 "" + echo "=== Last 100 lines of Wokwi CLI log ===" + tail -100 logs/wokwi.log || true + echo "" + echo "=== Last 100 lines of Serial output ===" + tail -100 logs/serial.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 "" + echo "=== Last 100 lines of Wokwi CLI log ===" + tail -100 logs/wokwi.log || true + echo "" + echo "=== Last 100 lines of Serial output ===" + tail -100 logs/serial.log || true + kill $WOKWI_PID || true + exit 1 + fi + + echo "WLED is ready for testing!" + echo "" + echo "=== First 50 lines of Serial output ===" + head -50 logs/serial.log || true + + - 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..8315be3309 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,17 @@ 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/bootloader.bin +/test/wokwi/partitions.bin +/test/wokwi/.wokwi/ +/test/wokwi/logs/ + diff --git a/package-lock.json b/package-lock.json index ee40deced7..89fb1bbeda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "14.7.0-mdev", "license": "EUPL-1.2", "dependencies": { + "@playwright/test": "^1.40.0", "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "nodemon": "^3.1.9", @@ -76,6 +77,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -620,6 +636,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "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==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/package.json b/package.json index 2bd32fb12c..52a604bc03 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ }, "scripts": { "build": "node tools/cdata.js", - "test": "node --test", - "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js" + "test": "node --test tools/cdata-test.js", + "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js", + "test:cdata": "node tools/cdata-test.js", + "test:wokwi-cdata": "node tools/wokwi-test.js", + "test:wokwi": "playwright test test/playwright/wokwi-basic.spec.js" }, "repository": { "type": "git", @@ -26,7 +29,8 @@ "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "web-resource-inliner": "^7.0.0", - "nodemon": "^3.1.9" + "nodemon": "^3.1.9", + "@playwright/test": "^1.40.0" }, "engines": { "node": ">=20.0.0" diff --git a/platformio.ini b/platformio.ini index d26b9fa7e8..9f5d3a0cc0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2025,6 +2025,64 @@ monitor_filters = esp32_exception_decoder ; RAM: [=== ] 26.4% (used 86356 bytes from 327680 bytes) ; Flash: [======== ] 83.6% (used 1753461 bytes from 2097152 bytes) + +## for testing with Wokwi +[env:esp32_V4_wokwi_debug] +extends = esp32_4MB_V4_M_base +;; platform = ${esp32.platformTasmota} +;; platform_packages = ${esp32.platform_packagesTasmota} +;board = esp32_16MB-poe ;; needed for ethernet boards (selects "esp32-poe" as variant) +;;board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +board_build.partitions = ${esp32.big_partitions} +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + ;; removing some usermods to keep it simple + -D USERMOD_DALLASTEMPERATURE + -D USERMOD_FOUR_LINE_DISPLAY + ;;-D USERMOD_ARTIFX + -D USERMOD_ROTARY_ENCODER_UI + -D USERMOD_AUTO_SAVE + -D USERMOD_PIRSWITCH + -D USERMOD_MULTI_RELAY + -D USE_ALT_DISPLAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D USERMOD_MPU6050_IMU ; gyro/accelero for USERMOD_GAMES (ONLY WORKS IF USERMOD_FOUR_LINE_DISPLAY NOT INCLUDED - I2C SHARING BUG) + -D USERMOD_GAMES ; WLEDMM usermod + ${common_mm.animartrix_build_flags} + ${common_mm.HUB75_build_flags} + -D WLED_DEBUG_HOST='"192.168.x.x"' ;; to disable net print + ;; more debug output + -DCORE_DEBUG_LEVEL=0 + -DNDEBUG + ;;${Speed_Flags.build_unflags} ;; to override -Os +build_flags = ${esp32_4MB_V4_S_base.build_flags} + ${common_mm.build_disable_sync_interfaces} + -D WLED_RELEASE_NAME=esp32_16MB_Wokwi_debug ; This will be included in the firmware.bin filename + -D SERVERNAME='"WLED-WOKWI"' + ;;${Speed_Flags.build_flags_V4} ;; optimize for speed + -g3 -ggdb ;; better debug output + -DCORE_DEBUG_LEVEL=5 ;; max core debug output + -DDEBUG -D WLED_DEBUG -DWLED_DEBUG_JSON ;; -DWLED_DEBUG_FS ;; max WLED debugging output + -D WLED_DISABLE_BROWNOUT_DET -D WLED_WATCHDOG_TIMEOUT=0 + ;;-D WLED_USE_ETHERNET + ;; -D WLED_ETH_DEFAULT=2 ;; ESP32-POE board configuration (works with QEMU open_eth) + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + -D WLED_DISABLE_OTA + -D WLED_DISABLE_ADALIGHT ;; WLEDMM Better to disable serial protocols, to avoid crashes (see upstream #3128) + -D MDNS_NAME=\"\" ;; disable MDNS + -D WLED_DISABLE_INFRARED + -D LEDPIN=4 + -D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1 ;; disable all extra pins + -D SR_DMTYPE=254 -D AUDIOPIN=-1 ;; set AR into "received only" mode +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + OneWire ; used for USERMOD_FOUR_LINE_DISPLAY and USERMOD_DALLASTEMPERATURE + U8g2 ; used for USERMOD_FOUR_LINE_DISPLA + ${common_mm.HUB75_lib_ignore} +monitor_filters = esp32_exception_decoder +; RAM: [=== ] 25.7% (used 84372 bytes from 327680 bytes) +; Flash: [========= ] 85.3% (used 1621925 bytes from 1900544 bytes) + + ;; experimental environment for boards with PSRAM (needs ESP-IDF 4.4.1). HUB75 included (may have PIN conflicts) [env:esp32_4MB_PSRAM_S] extends = esp32_4MB_V4_S_base 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..5387ab0b1e --- /dev/null +++ b/test/wokwi/README.md @@ -0,0 +1,235 @@ +# 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) with serial monitor settings +- `wokwi.toml` - Wokwi CLI configuration, flash files, and port forwarding +- `prepare-firmware.sh` - Script to copy built firmware, bootloader, and partitions to test directory +- `run-simulator.sh` - Script to start the Wokwi simulator +- `firmware.bin` - Main firmware binary (copied from build) +- `firmware.elf` - Firmware with debug symbols (copied from build) +- `bootloader.bin` - ESP32 bootloader (copied from build, flashed at 0x1000) +- `partitions.bin` - Partition table (copied from build, flashed at 0x8000) + +## Flash Files Configuration + +The simulator requires multiple binary files to properly emulate ESP32 boot and filesystem: + +**wokwi.toml flash configuration:** +```toml +[wokwi] +firmware = "firmware.bin" # Main application code +elf = "firmware.elf" # Debug symbols +partitions = "partitions.bin" # Partition table + +[[wokwi.flashFiles]] +offset = 0x1000 # Bootloader location +file = "bootloader.bin" + +[[wokwi.flashFiles]] +offset = 0x8000 # Partition table location +file = "partitions.bin" +``` + +**Why these files are needed:** +- `bootloader.bin` - ESP32 second-stage bootloader, loads the application +- `partitions.bin` - Partition table defining flash memory layout (app, SPIFFS, etc.) +- Without these, filesystem operations will fail with "partition not found" errors +- Standard ESP32 flash layout: bootloader@0x1000, partitions@0x8000, app@0x10000 + +## Serial Monitor Configuration + +The `diagram.json` file includes critical serial monitor configuration required for capturing firmware output in CI environments: + +```json +"serialMonitor": { + "display": "always", + "newline": "lf" +} +``` + +**Why this is needed:** +- Without `display: "always"`, serial output is not captured in headless/CI mode +- The Wokwi simulator only records serial output when explicitly configured +- This is **required** for debugging boot issues and verifying firmware execution + +**Configuration options:** +- `display: "always"` - Ensures serial output is captured even in headless mode (CI) +- `display: "auto"` - Only shows serial monitor when running interactively (not suitable for CI) +- `newline: "lf"` - Line ending format (LF for Unix-style, CRLF for Windows) + +**Troubleshooting missing serial output:** +If you see empty serial logs (`boot-check-serial.log` or `serial.log`): +1. Verify `serialMonitor` section exists in `diagram.json` +2. Check firmware.bin has valid ESP32 header (starts with `0xe9` magic byte) +3. Ensure firmware was built successfully and copied to test directory +4. Review firmware build logs for compilation errors + +## 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 esp32_V4_wokwi_debug + ``` + +3. Prepare firmware for testing: + ```bash + ./test/wokwi/prepare-firmware.sh esp32_V4_wokwi_debug + ``` + +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 + +## Boot Validation Scenarios + +Wokwi CLI supports test scenarios that can validate firmware boot without requiring a full Playwright test suite. Two scenarios are provided: + +### Quick Boot Check (`scenarios/boot-check.yaml`) +A fast 15-second validation that ensures WLED boots without immediate crashes. + +**Features:** +- Simple delay-based validation +- Total runtime: ~15 seconds +- Fails if simulator crashes or hangs during boot +- Perfect for CI pre-flight checks +- Tolerates transient network issues (code 1006 errors) + +**Usage:** +```bash +cd test/wokwi +~/.wokwi-ci/bin/wokwi-cli --timeout 30000 --scenario scenarios/boot-check.yaml . +``` + +**Note:** The CLI timeout (30 seconds) is set higher than the scenario timeout (15 seconds) to account for network delays and API connection time. + +### Comprehensive Boot Validation (`scenarios/boot-full.yaml`) +A thorough 30-second validation with extended timing for WiFi AP and HTTP server initialization. + +**Features:** +- Allows full system initialization +- Total runtime: ~30 seconds +- More detailed validation +- Better for local testing and troubleshooting + +**Usage:** +```bash +cd test/wokwi +~/.wokwi-ci/bin/wokwi-cli --timeout 40000 --scenario scenarios/boot-full.yaml . +``` + +### Creating Custom Scenarios + +You can create your own scenario files in YAML format: + +```yaml +name: "Custom Test" +version: 1 +timeout: 15000 # milliseconds + +steps: + - name: "Description of step" + delay: 5s # wait 5 seconds (note: time units required!) +``` + +The scenario will fail if: +- The simulator crashes during execution +- The timeout is exceeded +- Any step encounters an error + +## 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 +4. Create custom scenario files in `scenarios/` directory + +## 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 + +### No serial output from firmware +- Verify `serialMonitor` configuration in diagram.json +- Check firmware.bin is valid: + ```bash + hexdump -C firmware.bin | head -4 + # Should show ESP32 magic byte 0xe9 at start + ``` +- Ensure firmware was copied: `ls -lh test/wokwi/firmware.bin` +- Check firmware build 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..e43f2ecca7 --- /dev/null +++ b/test/wokwi/diagram.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "author": "WLED-MM CI", + "editor": "wokwi", + "parts": [ + { + "type": "wokwi-esp32-devkit-v1", + "id": "esp", + "top": 0, + "left": 0, + "attrs": {} + } + ], + "connections": [ + [ "esp:TX0", "$serialMonitor:RX", "", [] ], + [ "esp:RX0", "$serialMonitor:TX", "", [] ] + ], + "dependencies": {}, + "serialMonitor": { + "display": "always", + "newline": "lf" + } +} diff --git a/test/wokwi/prepare-firmware.sh b/test/wokwi/prepare-firmware.sh new file mode 100755 index 0000000000..fb40fd7b67 --- /dev/null +++ b/test/wokwi/prepare-firmware.sh @@ -0,0 +1,62 @@ +#!/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 esp32_V4_wokwi_debug" + exit 1 +fi + +ENV_NAME=$1 +BUILD_DIR="$PROJECT_ROOT/.pio/build/$ENV_NAME" +FIRMWARE_BIN="$BUILD_DIR/firmware.bin" +FIRMWARE_ELF="$BUILD_DIR/firmware.elf" +BOOTLOADER_BIN="$BUILD_DIR/bootloader.bin" +PARTITIONS_BIN="$BUILD_DIR/partitions.bin" + +# 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" +echo "✓ Copied firmware.bin" + +if [ -f "$FIRMWARE_ELF" ]; then + cp "$FIRMWARE_ELF" "$WOKWI_DIR/firmware.elf" + echo "✓ Copied firmware.elf" +else + echo "⚠ Warning: firmware.elf not found" +fi + +# Copy bootloader and partitions (required for filesystem support) +if [ -f "$BOOTLOADER_BIN" ]; then + cp "$BOOTLOADER_BIN" "$WOKWI_DIR/bootloader.bin" + echo "✓ Copied bootloader.bin" +else + echo "⚠ Warning: bootloader.bin not found at $BOOTLOADER_BIN" +fi + +if [ -f "$PARTITIONS_BIN" ]; then + cp "$PARTITIONS_BIN" "$WOKWI_DIR/partitions.bin" + echo "✓ Copied partitions.bin" +else + echo "⚠ Warning: partitions.bin not found at $PARTITIONS_BIN" +fi + +echo "" +echo "Firmware prepared successfully!" +echo "Files in $WOKWI_DIR:" +ls -lh "$WOKWI_DIR"/*.bin "$WOKWI_DIR"/*.elf 2>/dev/null || true diff --git a/test/wokwi/run-simulator.sh b/test/wokwi/run-simulator.sh new file mode 100755 index 0000000000..9fae3858be --- /dev/null +++ b/test/wokwi/run-simulator.sh @@ -0,0 +1,31 @@ +#!/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:-300} + +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" +echo "Serial output will be displayed below" +echo "==================================" +echo "" + +# Run wokwi-cli with timeout (in milliseconds) and scenario flag for better output +# 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 +# Serial output goes to stdout, diagnostic messages go to stderr +wokwi-cli --timeout ${WOKWI_TIMEOUT}000 . diff --git a/test/wokwi/scenarios/boot-check.yaml b/test/wokwi/scenarios/boot-check.yaml new file mode 100644 index 0000000000..caa7eb270f --- /dev/null +++ b/test/wokwi/scenarios/boot-check.yaml @@ -0,0 +1,21 @@ +# Quick Boot Check Scenario for WLED-MM +# This scenario performs a fast validation that WLED boots without immediate crashes +# Total runtime: ~15 seconds +# Perfect for CI pre-flight checks + +name: "WLED Quick Boot Check" +version: 1 +timeout: 15000 # 15 seconds - increased to allow for slower CI environments + +# Simple validation: just delay and ensure the simulator doesn't crash +steps: + - name: "Wait for initial boot" + delay: 6s # 6 seconds + + - name: "Verify simulator is still running" + delay: 6s # 6 seconds + + - name: "Final stability check" + delay: 3s # 3 seconds + +# If we get here without the simulator crashing, boot is successful diff --git a/test/wokwi/scenarios/boot-full.yaml b/test/wokwi/scenarios/boot-full.yaml new file mode 100644 index 0000000000..335d0a3cb5 --- /dev/null +++ b/test/wokwi/scenarios/boot-full.yaml @@ -0,0 +1,24 @@ +# Comprehensive Boot Validation Scenario for WLED-MM +# This scenario performs thorough validation with extended timing for WiFi AP and HTTP server +# Total runtime: ~30 seconds +# Better for local testing and troubleshooting + +name: "WLED Comprehensive Boot Validation" +version: 1 +timeout: 30000 # 30 seconds + +# Extended validation with more time for full system initialization +steps: + - name: "Initial boot phase" + delay: 8s # 8 seconds - ESP32 basic initialization + + - name: "WiFi AP initialization" + delay: 10s # 10 seconds - WiFi access point setup + + - name: "HTTP server startup" + delay: 8s # 8 seconds - Web server initialization + + - name: "Final stability verification" + delay: 4s # 4 seconds - Ensure everything is stable + +# If we get here, WLED has fully initialized with WiFi AP and HTTP server diff --git a/test/wokwi/wokwi.toml b/test/wokwi/wokwi.toml new file mode 100644 index 0000000000..bf693ae41b --- /dev/null +++ b/test/wokwi/wokwi.toml @@ -0,0 +1,21 @@ +[wokwi] +version = 1 +firmware = "firmware.bin" +elf = "firmware.elf" +partitions = "partitions.bin" +gdbServerPort=3333 + +# Flash additional files at specific offsets +# These are required for ESP32 to boot properly with filesystem support +[[wokwi.flashFiles]] +offset = 0x1000 +file = "bootloader.bin" + +[[wokwi.flashFiles]] +offset = 0x8000 +file = "partitions.bin" + +[[net.forward]] +# Forward the web server port +from = "localhost:8080" +to = "target:80" diff --git a/tools/wokwi-test.js b/tools/wokwi-test.js new file mode 100644 index 0000000000..29391a41b2 --- /dev/null +++ b/tools/wokwi-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);