name: Responsive UI Tests on: workflow_call: workflow_dispatch: # No concurrency group — intentionally omitted. # This workflow runs only via workflow_call (from release.yml's # responsive-test-gate) and workflow_dispatch. The pull_request trigger # was deliberately removed in #2248 to reduce PR CI load on what is a # heavy ~20-minute matrix build (mobile + desktop). A shared concurrency # key here previously caused workflow_call invocations to cancel each # other mid-flight; see #3554 (reverted in #3599) for that history. permissions: contents: read jobs: ui-tests: runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: viewport: [mobile, desktop] # Test mobile and desktop on each PR services: postgres: image: postgres:14@sha256:ca25035f7e6f74552655a1c5e4a9eb21f85e9d316f1f70371f790ef70095dd58 # v14 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: ldr_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up PDM uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5 with: python-version: '3.12' - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' cache: 'npm' cache-dependency-path: tests/ui_tests/package-lock.json - name: Free up disk space run: | # Remove unnecessary large packages to prevent disk space issues during cache save sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc sudo rm -rf /opt/hostedtoolcache/CodeQL df -h - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ wget \ gnupg \ ca-certificates \ fonts-liberation \ libasound2t64 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libcups2 \ libdbus-1-3 \ libdrm2 \ libgbm1 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libx11-xcb1 \ libxcomposite1 \ libxdamage1 \ libxrandr2 \ xdg-utils \ imagemagick - name: Install Python dependencies run: pdm install - name: Install root frontend dependencies run: npm ci - name: Build Vite frontend bundle # Generates src/local_deep_research/web/static/dist/, which the # Flask app loads via vite_helper. Without this the responsive UI # tests run against an unstyled page (no styles.css), which means # any CSS source changes between PRs are invisible to the test # baseline. See follow-up to PR #3985 for context. run: npm run build - name: Install Node test dependencies working-directory: tests/ui_tests run: | npm ci # Keep this version in sync with tests/ui_tests/package-lock.json's # puppeteer entry. A mismatch makes npx fetch a second puppeteer # that targets a different Chrome build, corrupting the browser # cache ("folder exists but executable is missing"). Matching the # locked version lets npx reuse the puppeteer just installed above. npx puppeteer@25.1.0 browsers install chrome - name: Set up test directories run: | mkdir -p ${{ github.workspace }}/data/encrypted_databases mkdir -p tests/ui_tests/screenshots echo "Created data and screenshots directories for tests" - name: Start test server env: CI: true FLASK_ENV: testing TEST_ENV: true SECRET_KEY: test-secret-key-for-ci # Security: CI test credential, not production secret LDR_DISABLE_RATE_LIMITING: true LDR_DATA_DIR: ${{ github.workspace }}/data DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ldr_test # Security: CI test database credentials, not production secrets run: | cd src # Start server and get its PID pdm run python -m local_deep_research.web.app > server.log 2>&1 & SERVER_PID=$! echo "Server PID: $SERVER_PID" # Persist the PID for the "Stop application server" step. That step # runs at the workspace root (no working-directory) while we are in # src/ here, so write an absolute path — otherwise the pidfile never # lines up and cleanup is a silent no-op. echo "$SERVER_PID" > "$GITHUB_WORKSPACE/server.pid" # Wait for server to start SERVER_READY=false for i in {1..60}; do # Probe the health endpoint, not bare `/`: `/` 302-redirects to # /auth/login and `curl -f` treats a 302 as success the instant # the socket binds — before DB/app init finishes — so it can pass # while the app is still not ready. /api/v1/health is a true # readiness signal and matches the docker-tests.yml gate. if curl -fsS --connect-timeout 2 --max-time 5 http://127.0.0.1:5000/api/v1/health 2>/dev/null; then echo "Server is ready after $i seconds" SERVER_READY=true break fi # Check if process is still running if ! kill -0 "$SERVER_PID" 2>/dev/null; then echo "Server process died!" echo "Server log:" cat server.log exit 1 fi echo "Waiting for server... ($i/60)" sleep 1 done # Fail fast if the server is alive but never became ready in time. # Without this guard the loop fell through silently on a slow-but- # not-crashed start, so "Register CI test user" and the UI tests ran # against a server that was not yet listening and failed with a # confusing `net::ERR_CONNECTION_REFUSED` in the *test* step instead # of a clear startup failure here. Window was also bumped 30s -> 60s # to match the proven puppeteer-e2e-tests.yml / docker-tests.yml # startup gates (Postgres-backed boot can exceed 30s on a loaded # runner). if [ "$SERVER_READY" != "true" ]; then echo "❌ Server did not become ready within 60 seconds" echo "Server log:" cat server.log exit 1 fi - name: Register CI test user working-directory: tests/ui_tests run: node register_ci_user.js http://127.0.0.1:5000 - name: Run responsive UI tests - ${{ matrix.viewport }} id: run-tests working-directory: tests/ui_tests env: VIEWPORT: ${{ matrix.viewport }} run: | set +e # Don't exit on test failure # Run tests and capture output HEADLESS=true node test_responsive_ui_comprehensive.js "$VIEWPORT" 2>&1 | tee test-output.log TEST_EXIT_CODE="${PIPESTATUS[0]}" # Extract summary for PR comment echo "### 📱 $VIEWPORT Test Results" > test-summary.md echo "" >> test-summary.md # Extract pass/fail counts PASSED=$(grep -oP '\d+(?= passed)' test-output.log | tail -1 || echo "0") FAILED=$(grep -oP '\d+(?= failed)' test-output.log | tail -1 || echo "0") WARNINGS=$(grep -oP '\d+(?= warnings)' test-output.log | tail -1 || echo "0") if [ "$FAILED" -eq "0" ]; then echo "✅ **All tests passed!**" >> test-summary.md else echo "❌ **$FAILED critical issues found**" >> test-summary.md fi { echo "" echo "- ✅ Passed: $PASSED" echo "- ❌ Failed: $FAILED" echo "- ⚠️ Warnings: $WARNINGS" } >> test-summary.md { echo "test_exit_code=$TEST_EXIT_CODE" echo "test_failed=$FAILED" } >> "$GITHUB_OUTPUT" # Only fail if critical failures exit "$TEST_EXIT_CODE" - name: Upload viewport test results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ui-test-results-${{ matrix.viewport }} path: | tests/ui_tests/test-output.log tests/ui_tests/test-summary.md tests/ui_tests/screenshots/ tests/ui_tests/responsive/ src/server.log retention-days: 7 if-no-files-found: ignore # NOTE: Earlier screenshot generation/upload steps were removed to fix actionlint warnings # caused by "if: false" disablement. Screenshots are now captured into the artifact above # when they exist. Visual regression workflows can re-enable dedicated screenshot steps later. - name: Upload to GitHub Pages (if available) if: always() && github.event_name == 'pull_request' continue-on-error: true env: PR_NUMBER: ${{ github.event.pull_request.number }} VIEWPORT: ${{ matrix.viewport }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPOSITORY: ${{ github.repository }} GH_REPO_OWNER: ${{ github.repository_owner }} GH_REPO_NAME: ${{ github.event.repository.name }} run: | # This requires GitHub Pages to be enabled for the repo # Create a branch for the screenshots BRANCH_NAME="pr-screenshots-$PR_NUMBER-$VIEWPORT" # Check if screenshots directory exists if [ ! -d "tests/ui_tests/responsive" ]; then echo "No screenshots directory found, skipping upload" exit 0 fi cd tests/ui_tests/responsive git init git config user.name "GitHub Actions" git config user.email "actions@github.com" # Add all screenshots and gallery git add responsive-ui-tests screenshot-gallery.html git commit -m "Screenshots for PR #$PR_NUMBER" # Push to a dedicated branch (requires write permissions) git push --force "https://x-access-token:$GH_TOKEN@github.com/$GH_REPOSITORY.git" "HEAD:$BRANCH_NAME" 2>/dev/null || true # The URL would be: https://[owner].github.io/[repo]/pr-screenshots-[number]-[viewport]/screenshot-gallery.html echo "Screenshots available at: https://$GH_REPO_OWNER.github.io/$GH_REPO_NAME/$BRANCH_NAME/screenshot-gallery.html" || true - name: Stop application server if: always() run: | if [ -f server.pid ]; then kill "$(cat server.pid)" || true rm server.pid fi # `pdm run` stays alive as a wrapper, so the pidfile holds the # wrapper PID and killing it can orphan the actual python server. # Add the same pkill fallback the webkit workflow uses so the # python process (and port 5000) is actually released. pkill -f "python -m local_deep_research.web.app" || true post-results: needs: ui-tests runs-on: ubuntu-latest # Workflow has no pull_request trigger; gating on event_name == 'pull_request' kept this job # from ever running. Always run so the combined report is built for workflow_dispatch / release. if: always() steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Download all artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: test-artifacts/ - name: Generate combined report run: | # Create a combined markdown report with screenshot links { echo "# 📊 Responsive UI Test Report" echo "" echo "## Test Summary" echo "" } > combined-report.md # Process each viewport's results for dir in test-artifacts/ui-test-results-*/; do if [ -d "$dir" ]; then viewport=$(basename "$dir" | sed 's/ui-test-results-//') echo "### $viewport" >> combined-report.md if [ -f "$dir/test-summary.md" ]; then cat "$dir/test-summary.md" >> combined-report.md fi # Count screenshots screenshot_count=$(find "$dir" -name "*.png" 2>/dev/null | wc -l) if [ "$screenshot_count" -gt "0" ]; then { echo "" echo "📸 **$screenshot_count screenshots captured**" } >> combined-report.md fi echo "" >> combined-report.md fi done { echo "## 📥 Download Options" echo "" echo "- [Download all test artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" echo "- View artifacts in the 'Artifacts' section below the workflow summary" } >> combined-report.md - name: Upload combined report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: combined-test-report path: combined-report.md retention-days: 30