Enhance testing framework and UI feedback

- Updated architecture documentation to include details on UI rendering checks and Playwright end-to-end tests.
- Revised testing documentation to specify Playwright for frontend E2E tests and added details on running tests.
- Implemented feedback mechanism in scenario form for successful creation notifications.
- Added feedback div in ScenarioForm.html for user notifications.
- Created new fixtures for Playwright tests to manage server and browser instances.
- Developed comprehensive E2E tests for consumption, costs, equipment, maintenance, production, and scenarios.
- Added smoke tests to verify UI page loading and form submissions.
- Enhanced unit tests for simulation and validation, including new tests for report generation and validation errors.
- Created new test files for router validation to ensure consistent error handling.
- Established a new test suite for UI routes to validate dashboard and reporting functionalities.
- Implemented validation tests to ensure proper handling of JSON payloads.
This commit is contained in:
2025-10-21 08:29:11 +02:00
parent ae4b9c136f
commit f020d276bc
20 changed files with 939 additions and 21 deletions

49
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,49 @@
import subprocess
import time
from typing import Generator
import pytest
from playwright.sync_api import Browser, Page, Playwright, sync_playwright
# Use a different port for the test server to avoid conflicts
TEST_PORT = 8001
BASE_URL = f"http://localhost:{TEST_PORT}"
@pytest.fixture(scope="function")
def live_server() -> Generator[str, None, None]:
"""Launch a live test server in a separate process."""
process = subprocess.Popen(
["uvicorn", "main:app", f"--port={TEST_PORT}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
time.sleep(2) # Give the server a moment to start
yield BASE_URL
process.terminate()
process.wait()
@pytest.fixture(scope="session")
def playwright_instance() -> Generator[Playwright, None, None]:
"""Provide a Playwright instance for the test session."""
with sync_playwright() as p:
yield p
@pytest.fixture(scope="session")
def browser(
playwright_instance: Playwright,
) -> Generator[Browser, None, None]:
"""Provide a browser instance for the test session."""
browser = playwright_instance.chromium.launch()
yield browser
browser.close()
@pytest.fixture()
def page(browser: Browser, live_server: str) -> Generator[Page, None, None]:
"""Provide a new page for each test."""
page = browser.new_page(base_url=live_server)
yield page
page.close()

View File

@@ -0,0 +1,42 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_consumption_form_loads(page: Page):
"""Verify the consumption form page loads correctly."""
page.goto("/ui/consumption")
expect(page).to_have_title("CalMiner Consumption")
expect(page.locator("h1")).to_have_text("Consumption")
def test_create_consumption_item(page: Page):
"""Test creating a new consumption item through the UI."""
# First, create a scenario to associate the consumption with.
page.goto("/ui/scenarios")
scenario_name = f"Consumption Test Scenario {uuid4()}"
page.fill("input[name='name']", scenario_name)
page.click("button[type='submit']")
with page.expect_response("**/api/scenarios/"):
pass # Wait for the scenario to be created
# Now, navigate to the consumption page and add an item.
page.goto("/ui/consumption")
# Create a consumption item.
consumption_desc = "Diesel for generators"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='description']", consumption_desc)
page.fill("input[name='amount']", "5000")
page.click("button[type='submit']")
with page.expect_response("**/api/consumption/") as response_info:
pass
assert response_info.value.status == 201
# Verify the new item appears in the table.
expect(page.locator(f"tr:has-text('{consumption_desc}')")).to_be_visible()
# Verify the feedback message.
expect(page.locator("#consumption-feedback")
).to_have_text("Consumption record saved.")

58
tests/e2e/test_costs.py Normal file
View File

@@ -0,0 +1,58 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_costs_form_loads(page: Page):
"""Verify the costs form page loads correctly."""
page.goto("/ui/costs")
expect(page).to_have_title("CalMiner Costs")
expect(page.locator("h1")).to_have_text("Costs")
def test_create_capex_and_opex_items(page: Page):
"""Test creating new CAPEX and OPEX items through the UI."""
# First, create a scenario to associate the costs with.
page.goto("/ui/scenarios")
scenario_name = f"Cost Test Scenario {uuid4()}"
page.fill("input[name='name']", scenario_name)
page.click("button[type='submit']")
with page.expect_response("**/api/scenarios/"):
pass # Wait for the scenario to be created
# Now, navigate to the costs page and add CAPEX and OPEX items.
page.goto("/ui/costs")
# Create a CAPEX item.
capex_desc = "Initial drilling equipment"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='description']", capex_desc)
page.fill("input[name='amount']", "150000")
page.click("#capex-form button[type='submit']")
with page.expect_response("**/api/costs/capex") as response_info:
pass
assert response_info.value.status == 200
# Create an OPEX item.
opex_desc = "Monthly fuel costs"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='description']", opex_desc)
page.fill("input[name='amount']", "25000")
page.click("#opex-form button[type='submit']")
with page.expect_response("**/api/costs/opex") as response_info:
pass
assert response_info.value.status == 200
# Verify the new items appear in their respective tables.
expect(page.locator(
f"#capex-table tr:has-text('{capex_desc}')")).to_be_visible()
expect(page.locator(
f"#opex-table tr:has-text('{opex_desc}')")).to_be_visible()
# Verify the feedback messages.
expect(page.locator("#capex-feedback")
).to_have_text("Entry saved successfully.")
expect(page.locator("#opex-feedback")
).to_have_text("Entry saved successfully.")

View File

@@ -0,0 +1,18 @@
from playwright.sync_api import Page, expect
def test_dashboard_loads_and_has_title(page: Page):
"""Verify the dashboard page loads and the title is correct."""
expect(page).to_have_title("CalMiner Dashboard")
def test_dashboard_shows_summary_metrics_panel(page: Page):
"""Check that the summary metrics panel is visible."""
summary_panel = page.locator("section.panel h2:has-text('Summary Metrics')")
expect(summary_panel).to_be_visible()
def test_dashboard_renders_cost_chart(page: Page):
"""Ensure the scenario cost chart canvas is present."""
cost_chart = page.locator("#scenario-cost-chart")
expect(cost_chart).to_be_visible()

View File

@@ -0,0 +1,43 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_equipment_form_loads(page: Page):
"""Verify the equipment form page loads correctly."""
page.goto("/ui/equipment")
expect(page).to_have_title("CalMiner Equipment")
expect(page.locator("h1")).to_have_text("Equipment")
def test_create_equipment_item(page: Page):
"""Test creating a new equipment item through the UI."""
# First, create a scenario to associate the equipment with.
page.goto("/ui/scenarios")
scenario_name = f"Equipment Test Scenario {uuid4()}"
page.fill("input[name='name']", scenario_name)
page.click("button[type='submit']")
with page.expect_response("**/api/scenarios/"):
pass # Wait for the scenario to be created
# Now, navigate to the equipment page and add an item.
page.goto("/ui/equipment")
# Create an equipment item.
equipment_name = "Haul Truck HT-05"
equipment_desc = "Primary haul truck for ore transport."
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='name']", equipment_name)
page.fill("textarea[name='description']", equipment_desc)
page.click("button[type='submit']")
with page.expect_response("**/api/equipment/") as response_info:
pass
assert response_info.value.status == 200
# Verify the new item appears in the table.
expect(page.locator(f"tr:has-text('{equipment_name}')")).to_be_visible()
# Verify the feedback message.
expect(page.locator("#equipment-feedback")
).to_have_text("Equipment saved.")

View File

@@ -0,0 +1,52 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_maintenance_form_loads(page: Page):
"""Verify the maintenance form page loads correctly."""
page.goto("/ui/maintenance")
expect(page).to_have_title("CalMiner Maintenance")
expect(page.locator("h1")).to_have_text("Maintenance")
def test_create_maintenance_item(page: Page):
"""Test creating a new maintenance item through the UI."""
# First, create a scenario and an equipment item.
page.goto("/ui/scenarios")
scenario_name = f"Maintenance Test Scenario {uuid4()}"
page.fill("input[name='name']", scenario_name)
page.click("button[type='submit']")
with page.expect_response("**/api/scenarios/"):
pass
page.goto("/ui/equipment")
equipment_name = f"Excavator EX-12 {uuid4()}"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='name']", equipment_name)
page.click("button[type='submit']")
with page.expect_response("**/api/equipment/"):
pass
# Now, navigate to the maintenance page and add an item.
page.goto("/ui/maintenance")
# Create a maintenance item.
maintenance_desc = "Scheduled engine overhaul"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.select_option("select[name='equipment_id']", label=equipment_name)
page.fill("input[name='maintenance_date']", "2025-12-01")
page.fill("textarea[name='description']", maintenance_desc)
page.fill("input[name='cost']", "12000")
page.click("button[type='submit']")
with page.expect_response("**/api/maintenance/") as response_info:
pass
assert response_info.value.status == 201
# Verify the new item appears in the table.
expect(page.locator(f"tr:has-text('{maintenance_desc}')")).to_be_visible()
# Verify the feedback message.
expect(page.locator("#maintenance-feedback")
).to_have_text("Maintenance entry saved.")

View File

@@ -0,0 +1,42 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_production_form_loads(page: Page):
"""Verify the production form page loads correctly."""
page.goto("/ui/production")
expect(page).to_have_title("CalMiner Production")
expect(page.locator("h1")).to_have_text("Production")
def test_create_production_item(page: Page):
"""Test creating a new production item through the UI."""
# First, create a scenario to associate the production with.
page.goto("/ui/scenarios")
scenario_name = f"Production Test Scenario {uuid4()}"
page.fill("input[name='name']", scenario_name)
page.click("button[type='submit']")
with page.expect_response("**/api/scenarios/"):
pass # Wait for the scenario to be created
# Now, navigate to the production page and add an item.
page.goto("/ui/production")
# Create a production item.
production_desc = "Ore extracted - Grade A"
page.select_option("select[name='scenario_id']", label=scenario_name)
page.fill("input[name='description']", production_desc)
page.fill("input[name='amount']", "1500")
page.click("button[type='submit']")
with page.expect_response("**/api/production/") as response_info:
pass
assert response_info.value.status == 201
# Verify the new item appears in the table.
expect(page.locator(f"tr:has-text('{production_desc}')")).to_be_visible()
# Verify the feedback message.
expect(page.locator("#production-feedback")
).to_have_text("Production output saved.")

View File

@@ -0,0 +1,8 @@
from playwright.sync_api import Page, expect
def test_reporting_view_loads(page: Page):
"""Verify the reporting view page loads correctly."""
page.click("a[href='/ui/reporting']")
expect(page).to_have_url("/ui/reporting")
expect(page.locator("h2:has-text('Reporting')")).to_be_visible()

View File

@@ -0,0 +1,42 @@
from uuid import uuid4
from playwright.sync_api import Page, expect
def test_scenario_form_loads(page: Page):
"""Verify the scenario form page loads correctly."""
page.goto("/ui/scenarios")
expect(page).to_have_url(
"http://localhost:8001/ui/scenarios"
) # Updated port
expect(page.locator("h2:has-text('Create New Scenario')")).to_be_visible()
def test_create_new_scenario(page: Page):
"""Test creating a new scenario via the UI form."""
page.goto("/ui/scenarios")
scenario_name = f"E2E Test Scenario {uuid4()}"
scenario_desc = "A scenario created during an end-to-end test."
page.fill("input[name='name']", scenario_name)
page.fill("textarea[name='description']", scenario_desc)
# Expect a network response from the POST request after clicking the submit button.
with page.expect_response("**/api/scenarios/") as response_info:
page.click("button[type='submit']")
response = response_info.value
assert response.status == 200
# After a successful submission, the new scenario should be visible in the table.
# The table is dynamically updated, so we might need to wait for it to appear.
new_row = page.locator(f"tr:has-text('{scenario_name}')")
expect(new_row).to_be_visible()
expect(new_row.locator("td").nth(1)).to_have_text(scenario_desc)
# Verify the feedback message.
feedback = page.locator("#feedback")
expect(feedback).to_be_visible()
expect(feedback).to_have_text(
f'Scenario "{scenario_name}" created successfully.')

29
tests/e2e/test_smoke.py Normal file
View File

@@ -0,0 +1,29 @@
import pytest
from playwright.sync_api import Page, expect
# A list of UI routes to check, with their URL, expected title, and a key heading text.
UI_ROUTES = [
("/", "CalMiner Dashboard", "Dashboard"),
("/ui/dashboard", "CalMiner Dashboard", "Dashboard"),
("/ui/scenarios", "CalMiner Scenarios", "Scenarios"),
("/ui/parameters", "CalMiner Parameters", "Parameters"),
("/ui/costs", "CalMiner Costs", "Costs"),
("/ui/consumption", "CalMiner Consumption", "Consumption"),
("/ui/production", "CalMiner Production", "Production"),
("/ui/equipment", "CalMiner Equipment", "Equipment"),
("/ui/maintenance", "CalMiner Maintenance", "Maintenance"),
("/ui/simulations", "CalMiner Simulations", "Simulations"),
("/ui/reporting", "CalMiner Reporting", "Reporting"),
]
@pytest.mark.usefixtures("live_server")
@pytest.mark.parametrize("url, title, heading", UI_ROUTES)
def test_ui_pages_load_correctly(page: Page, url: str, title: str, heading: str):
"""Verify that all UI pages load with the correct title and a visible heading."""
page.goto(url)
expect(page).to_have_title(title)
# The app uses a mix of h1 and h2 for main page headings.
heading_locator = page.locator(
f"h1:has-text('{heading}'), h2:has-text('{heading}')")
expect(heading_locator.first).to_be_visible()