diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d99c826..cf75b9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,11 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - name: Install browser test dependencies + run: | + npm ci + npx playwright install --with-deps chromium + - name: Verify assets are up to date run: | make clean @@ -47,6 +52,7 @@ jobs: run: | cargo test node --test tests/*.test.js + node tests/demo-browser-check.js - name: Verify package contents run: ./scripts/verify-package.sh diff --git a/.gitignore b/.gitignore index c275290..79c89d5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ Thumbs.db *.iml .zed/ +# Node +node_modules/ +__pycache__/ + # Claude .claude/ diff --git a/Makefile b/Makefile index 169c6d5..58ab821 100644 --- a/Makefile +++ b/Makefile @@ -27,8 +27,8 @@ VERSIONED_CSS := static/sf/sf.$(VERSION).css VERSIONED_JS := static/sf/sf.$(VERSION).js # ============== Phony Targets ============== -.PHONY: banner help assets build build-release test test-quick test-doc test-unit test-frontend test-one \ - lint fmt fmt-check clippy ci-local pre-release version package-verify \ +.PHONY: banner help assets build build-release test test-quick test-doc test-unit test-frontend test-browser test-one \ + lint fmt fmt-check clippy ci-local pre-release version package-verify browser-setup \ bump-patch bump-minor bump-major bump-dry demo-serve \ publish-dry publish clean watch @@ -85,6 +85,7 @@ test: banner @printf "$(ARROW) $(BOLD)Running all tests...$(RESET)\n" @cargo test && \ node --test tests/*.test.js && \ + node tests/demo-browser-check.js && \ printf "\n$(GREEN)$(CHECK) All tests passed$(RESET)\n\n" || \ (printf "\n$(RED)$(CROSS) Tests failed$(RESET)\n\n" && exit 1) @@ -104,6 +105,10 @@ test-quick: banner @node --test tests/*.test.js && \ printf "$(GREEN)$(CHECK) Frontend tests passed$(RESET)\n\n" || \ (printf "$(RED)$(CROSS) Frontend tests failed$(RESET)\n\n" && exit 1) + @printf "$(PROGRESS) Running browser demo smoke tests...\n" + @node tests/demo-browser-check.js && \ + printf "$(GREEN)$(CHECK) Browser smoke tests passed$(RESET)\n\n" || \ + (printf "$(RED)$(CROSS) Browser smoke tests failed$(RESET)\n\n" && exit 1) test-doc: @printf "$(PROGRESS) Running doctests...\n" @@ -123,6 +128,18 @@ test-frontend: printf "$(GREEN)$(CHECK) Frontend tests passed$(RESET)\n" || \ (printf "$(RED)$(CROSS) Frontend tests failed$(RESET)\n" && exit 1) +test-browser: + @printf "$(PROGRESS) Running browser demo smoke tests...\n" + @node tests/demo-browser-check.js && \ + printf "$(GREEN)$(CHECK) Browser smoke tests passed$(RESET)\n" || \ + (printf "$(RED)$(CROSS) Browser smoke tests failed$(RESET)\n" && exit 1) + +browser-setup: + @printf "$(PROGRESS) Installing browser test dependencies...\n" + @npm ci && npx playwright install --with-deps chromium && \ + printf "$(GREEN)$(CHECK) Browser test dependencies installed$(RESET)\n" || \ + (printf "$(RED)$(CROSS) Browser test dependency setup failed$(RESET)\n" && exit 1) + test-one: @printf "$(PROGRESS) Running test: $(YELLOW)$(TEST)$(RESET)\n" @RUST_LOG=info cargo test $(TEST) -- --nocapture @@ -156,20 +173,22 @@ ci-local: banner @printf "$(CYAN)$(BOLD)║ Local CI Simulation ║$(RESET)\n" @printf "$(CYAN)$(BOLD)╚══════════════════════════════════════════════════════════╝$(RESET)\n\n" @printf "$(ARROW) $(BOLD)Simulating GitHub Actions CI workflow locally...$(RESET)\n\n" - @printf "$(PROGRESS) Step 1/6: Asset freshness check...\n" + @printf "$(PROGRESS) Step 1/8: Asset freshness check...\n" @$(MAKE) assets --no-print-directory - @printf "$(PROGRESS) Step 2/6: Format check...\n" + @printf "$(PROGRESS) Step 2/8: Format check...\n" @$(MAKE) fmt-check --no-print-directory - @printf "$(PROGRESS) Step 3/6: Build...\n" + @printf "$(PROGRESS) Step 3/8: Build...\n" @cargo build --quiet && printf "$(GREEN)$(CHECK) Build passed$(RESET)\n" - @printf "$(PROGRESS) Step 4/6: Clippy...\n" + @printf "$(PROGRESS) Step 4/8: Clippy...\n" @$(MAKE) clippy --no-print-directory - @printf "$(PROGRESS) Step 5/6: Doctests...\n" + @printf "$(PROGRESS) Step 5/8: Doctests...\n" @cargo test --doc --quiet && printf "$(GREEN)$(CHECK) Doctests passed$(RESET)\n" - @printf "$(PROGRESS) Step 6/7: Unit tests...\n" + @printf "$(PROGRESS) Step 6/8: Unit tests...\n" @cargo test --lib --quiet && printf "$(GREEN)$(CHECK) Unit tests passed$(RESET)\n" - @printf "$(PROGRESS) Step 7/7: Frontend tests...\n" + @printf "$(PROGRESS) Step 7/8: Frontend tests...\n" @node --test tests/*.test.js && printf "$(GREEN)$(CHECK) Frontend tests passed$(RESET)\n" + @printf "$(PROGRESS) Step 8/8: Browser smoke tests...\n" + @node tests/demo-browser-check.js && printf "$(GREEN)$(CHECK) Browser smoke tests passed$(RESET)\n" @printf "\n$(GREEN)$(BOLD)╔══════════════════════════════════════════════════════════╗$(RESET)\n" @printf "$(GREEN)$(BOLD)║ $(CHECK) CI SIMULATION PASSED ║$(RESET)\n" @printf "$(GREEN)$(BOLD)╚══════════════════════════════════════════════════════════╝$(RESET)\n\n" @@ -209,7 +228,7 @@ pre-release: banner @$(MAKE) fmt-check --no-print-directory @$(MAKE) clippy --no-print-directory @printf "$(PROGRESS) Running full test suite...\n" - @cargo test --quiet && node --test tests/*.test.js && printf "$(GREEN)$(CHECK) All tests passed$(RESET)\n" + @cargo test --quiet && node --test tests/*.test.js && node tests/demo-browser-check.js && printf "$(GREEN)$(CHECK) All tests passed$(RESET)\n" @printf "$(PROGRESS) Dry-run publish...\n" @cargo publish --dry-run 2>&1 | tail -1 @printf "$(PROGRESS) Verifying packaged contents...\n" @@ -266,7 +285,7 @@ watch: demo-serve: assets @printf "$(ARROW) Serving demos at http://localhost:8000/demos/\n" - @python3 -m http.server 8000 + @python3 scripts/demo_server.py # ============== Help ============== diff --git a/README.md b/README.md index dc40a98..a89120a 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,8 @@ Runnable demo fixtures live in `demos/`. - `demos/full-surface.html` exercises the primary shipped component surface together. - `demos/rail.html` focuses on resource cards, blocks, gauges, and changeovers. - `make demo-serve` serves the repository at `http://localhost:8000/demos/` for local validation. +- `make test-browser` runs browser-level smoke tests against both demo fixtures. +- Run `make browser-setup` once on a machine to install the Playwright test dependency and Chromium. ## Acknowledgments diff --git a/demos/README.md b/demos/README.md index ff11db6..a5ea7fa 100644 --- a/demos/README.md +++ b/demos/README.md @@ -16,6 +16,22 @@ Then open: - `http://localhost:8000/demos/full-surface.html` - `http://localhost:8000/demos/rail.html` +## Automated browser verification + +Install the browser-test dependency and Chromium once: + +```bash +make browser-setup +``` + +Then run the smoke checks: + +```bash +make test-browser +``` + +The automated check serves the repository locally, opens both runnable demo fixtures in Chromium, fails on page or script errors, and verifies that the primary shipped UI surfaces mount successfully. + ## Coverage - `full-surface.html`: header, status bar, tabs, buttons, modal, toast, table, rail, Gantt, API guide, and footer diff --git a/demos/full-surface.html b/demos/full-surface.html index 1961fb3..01bb9c8 100644 --- a/demos/full-surface.html +++ b/demos/full-surface.html @@ -100,35 +100,11 @@ return card.el; } - function buildGantt() { + function createGanttPanel() { var mount = document.createElement('div'); mount.className = 'demo-gantt'; - - var gantt = SF.gantt.create({ - gridTitle: 'Tasks', - chartTitle: 'Schedule', - viewMode: 'Half Day', - splitSizes: [38, 62], - columns: [ - { key: 'name', label: 'Task' }, - { key: 'start', label: 'Start' }, - { key: 'end', label: 'End' }, - { key: 'priority', label: 'P', render: function (task) { - return 'P' + task.priority + ''; - } } - ], - onTaskClick: function (task) { - SF.showToast({ - title: 'Gantt task', - message: task.name, - variant: 'success', - delay: 1800 - }); - } - }); - - gantt.mount(mount); - gantt.setTasks([ + var mounted = false; + var tasks = [ { id: 'task-1', name: 'Design review', @@ -156,9 +132,41 @@ custom_class: 'project-color-2 priority-3', dependencies: 'task-2' } - ]); + ]; - return mount; + var gantt = SF.gantt.create({ + gridTitle: 'Tasks', + chartTitle: 'Schedule', + viewMode: 'Half Day', + splitSizes: [38, 62], + columns: [ + { key: 'name', label: 'Task' }, + { key: 'start', label: 'Start' }, + { key: 'end', label: 'End' }, + { key: 'priority', label: 'P', render: function (task) { + return 'P' + task.priority + ''; + } } + ], + onTaskClick: function (task) { + SF.showToast({ + title: 'Gantt task', + message: task.name, + variant: 'success', + delay: 1800 + }); + } + }); + + return { + mountIfNeeded: function () { + if (mounted) return; + if (!mount.isConnected || mount.offsetWidth === 0 || mount.offsetHeight === 0) return; + gantt.mount(mount); + gantt.setTasks(tasks); + mounted = true; + }, + el: mount + }; } function buildApiGuide() { @@ -186,6 +194,8 @@ } document.addEventListener('DOMContentLoaded', function () { + var ganttPanel = createGanttPanel(); + var header = SF.createHeader({ logo: '../static/sf/img/ouroboros.svg', title: 'Planner123', @@ -195,7 +205,14 @@ { id: 'gantt', label: 'Gantt', icon: 'fa-chart-gantt' }, { id: 'api', label: 'API', icon: 'fa-plug' } ], - onTabChange: function (id) { SF.showTab(id); }, + onTabChange: function (id) { + SF.showTab(id); + if (id === 'gantt') { + requestAnimationFrame(function () { + ganttPanel.mountIfNeeded(); + }); + } + }, actions: { onSolve: function () { statusBar.setSolving(true); @@ -280,7 +297,7 @@ var panel = document.createElement('section'); panel.className = 'demo-card'; panel.innerHTML = '