diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93585ee..a8e07d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,3 +33,23 @@ jobs: done < <(find . -name '*.js' -not -path './.git/*' -not -path './node_modules/*' -print0) [ "$found" = "1" ] || echo "No JS files found to check." echo "Static app validated." + + test: + name: playwright-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-playwright + playwright install chromium --with-deps + - name: Run tests + run: | + python3 -m http.server 8000 & + sleep 3 + pytest tests/test_app.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d34f089 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.playwright/ +ms-playwright/ diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..919637d --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,74 @@ +import pytest +import re +from playwright.sync_api import Page, expect + +@pytest.fixture(autouse=True) +def setup(page: Page): + # Go to the app. We assume the server is running on localhost:8000 + page.goto("http://localhost:8000/index.html?debug") + yield + +def test_page_load(page: Page): + """Verify that the page loads with the correct title and initial state.""" + expect(page).to_have_title("StepView — Step, Distance & Calorie Counter") + expect(page.locator("#steps")).to_have_text("0") + expect(page.locator("#distance")).to_have_text("0.00") + +def test_no_js_errors(page: Page): + """Verify that there are no JavaScript errors on page load.""" + errors = [] + page.on("pageerror", lambda err: errors.append(err.message)) + page.goto("http://localhost:8000/index.html?debug") + assert len(errors) == 0, f"JS errors found: {errors}" + +def test_simulate_steps(page: Page): + """Verify that simulating steps via debug hooks updates the UI.""" + page.evaluate("window.__sv.simSteps(10)") + expect(page.locator("#steps")).to_have_text("10") + # 10 steps at 0.71m/stride = 7.1m = 0.01km (rounded) + expect(page.locator("#distance")).to_have_text("0.01") + +def test_navigation(page: Page): + """Verify navigation between Home and Settings.""" + # Go to Settings + page.click("button[data-action='settings']") + expect(page.locator("#settings")).not_to_have_class(re.compile("hidden")) + expect(page.locator("#home")).to_have_class(re.compile("hidden")) + + # Go back to Home + page.click("button[data-action='back']") + expect(page.locator("#home")).not_to_have_class(re.compile("hidden")) + expect(page.locator("#settings")).to_have_class(re.compile("hidden")) + +def test_persistence(page: Page): + """Verify that steps are persisted across reloads.""" + page.evaluate("window.__sv.simSteps(50)") + expect(page.locator("#steps")).to_have_text("50") + + # Reload the page + page.reload() + # Wait for the app to initialize + expect(page.locator("#steps")).to_have_text("50") + +def test_settings_adjustment(page: Page): + """Verify that settings can be adjusted and they update the UI.""" + page.click("button[data-action='settings']") + + # Increase weight + initial_weight = page.locator("#set-weight").inner_text() + page.click("button[data-action='weight-inc']") + expect(page.locator("#set-weight")).to_have_text(str(int(initial_weight) + 1)) + + # Go back and check if it's reflected in calories (if we have steps) + page.click("button[data-action='back']") + page.evaluate("window.__sv.simSteps(1000)") # Significant steps to see calorie change + calories_before = page.locator("#calories").inner_text() + + page.click("button[data-action='settings']") + page.click("button[data-action='weight-inc']") # +1kg again + page.click("button[data-action='back']") + + # Calories = distance_km * weight_kg * 0.9 + # Increasing weight should increase calories + calories_after = page.locator("#calories").inner_text() + assert int(calories_after.replace(',', '')) > int(calories_before.replace(',', ''))