feat(sqlite): refactor orders schema + dashboard period filter
Replace import_orders (insert-per-run) with orders table (one row per order, upsert on conflict). Eliminates dedup CTE on every dashboard query and prevents unbounded row growth at 4-500 orders/sync. Key changes: - orders table: PK order_number, upsert via ON CONFLICT DO UPDATE; COALESCE preserves id_comanda once set; times_skipped auto-increments - sync_run_orders: lightweight junction (sync_run_id, order_number) replaces sync_run_id column on orders - order_items: PK changed to (order_number, sku), INSERT OR IGNORE - Auto-migration in init_sqlite(): import_orders → orders on first boot, old table renamed to import_orders_bak - /api/dashboard/orders: period_days param (3/7/30/0=all, default 7) - Dashboard: period selector buttons in orders card header - start.sh: stop existing process on port 5003 before restart; remove --reload (broken on WSL2 /mnt/e/) - Add invoice_service, E2E Playwright tests, Oracle package updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
82
api/tests/e2e/conftest.py
Normal file
82
api/tests/e2e/conftest.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Playwright E2E test fixtures.
|
||||
Starts the FastAPI app on a random port with test SQLite, no Oracle.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import pytest
|
||||
import subprocess
|
||||
import time
|
||||
import socket
|
||||
|
||||
|
||||
def _free_port():
|
||||
with socket.socket() as s:
|
||||
s.bind(('', 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_url():
|
||||
"""Start the FastAPI app as a subprocess and return its URL."""
|
||||
port = _free_port()
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
sqlite_path = os.path.join(tmpdir, "e2e_test.db")
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"FORCE_THIN_MODE": "true",
|
||||
"SQLITE_DB_PATH": sqlite_path,
|
||||
"ORACLE_DSN": "dummy",
|
||||
"ORACLE_USER": "dummy",
|
||||
"ORACLE_PASSWORD": "dummy",
|
||||
"JSON_OUTPUT_DIR": tmpdir,
|
||||
})
|
||||
|
||||
api_dir = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", str(port)],
|
||||
cwd=api_dir,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
# Wait for startup (up to 15 seconds)
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
for _ in range(30):
|
||||
try:
|
||||
import urllib.request
|
||||
urllib.request.urlopen(f"{url}/health", timeout=1)
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
proc.kill()
|
||||
stdout, stderr = proc.communicate()
|
||||
raise RuntimeError(
|
||||
f"App failed to start on port {port}.\n"
|
||||
f"STDOUT: {stdout.decode()[-2000:]}\n"
|
||||
f"STDERR: {stderr.decode()[-2000:]}"
|
||||
)
|
||||
|
||||
yield url
|
||||
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_test_data(app_url):
|
||||
"""
|
||||
Seed SQLite with test data via API calls.
|
||||
|
||||
Oracle is unavailable in E2E tests — only SQLite-backed pages are
|
||||
fully functional. This fixture exists as a hook for future seeding;
|
||||
for now E2E tests validate UI structure on empty-state pages.
|
||||
"""
|
||||
return app_url
|
||||
171
api/tests/e2e/test_dashboard_live.py
Normal file
171
api/tests/e2e/test_dashboard_live.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
E2E verification: Dashboard page against the live app (localhost:5003).
|
||||
|
||||
Run with:
|
||||
python -m pytest api/tests/e2e/test_dashboard_live.py -v --headed
|
||||
|
||||
This tests the LIVE app, not a test instance. Requires the app to be running.
|
||||
"""
|
||||
import pytest
|
||||
from playwright.sync_api import sync_playwright, Page, expect
|
||||
|
||||
BASE_URL = "http://localhost:5003"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def browser_page():
|
||||
"""Launch browser and yield a page connected to the live app."""
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
context = browser.new_context(viewport={"width": 1280, "height": 900})
|
||||
page = context.new_page()
|
||||
yield page
|
||||
browser.close()
|
||||
|
||||
|
||||
class TestDashboardPageLoad:
|
||||
"""Verify dashboard page loads and shows expected structure."""
|
||||
|
||||
def test_dashboard_loads(self, browser_page: Page):
|
||||
browser_page.goto(f"{BASE_URL}/")
|
||||
browser_page.wait_for_load_state("networkidle")
|
||||
expect(browser_page.locator("h4")).to_contain_text("Panou de Comanda")
|
||||
|
||||
def test_sync_control_visible(self, browser_page: Page):
|
||||
expect(browser_page.locator("#btnStartSync")).to_be_visible()
|
||||
expect(browser_page.locator("#syncStatusBadge")).to_be_visible()
|
||||
|
||||
def test_last_sync_card_populated(self, browser_page: Page):
|
||||
"""The lastSyncBody should show data from previous runs."""
|
||||
last_sync_date = browser_page.locator("#lastSyncDate")
|
||||
expect(last_sync_date).to_be_visible()
|
||||
text = last_sync_date.text_content()
|
||||
assert text and text != "-", f"Expected last sync date to be populated, got: '{text}'"
|
||||
|
||||
def test_last_sync_imported_count(self, browser_page: Page):
|
||||
imported_el = browser_page.locator("#lastSyncImported")
|
||||
text = imported_el.text_content()
|
||||
count = int(text) if text and text.isdigit() else 0
|
||||
assert count >= 0, f"Expected imported count >= 0, got: {text}"
|
||||
|
||||
def test_last_sync_status_badge(self, browser_page: Page):
|
||||
status_el = browser_page.locator("#lastSyncStatus .badge")
|
||||
expect(status_el).to_be_visible()
|
||||
text = status_el.text_content()
|
||||
assert text in ("completed", "running", "failed"), f"Unexpected status: {text}"
|
||||
|
||||
|
||||
class TestDashboardOrdersTable:
|
||||
"""Verify orders table displays data from SQLite."""
|
||||
|
||||
def test_orders_table_has_rows(self, browser_page: Page):
|
||||
"""Dashboard should show orders from previous sync runs."""
|
||||
browser_page.goto(f"{BASE_URL}/")
|
||||
browser_page.wait_for_load_state("networkidle")
|
||||
# Wait for the orders to load (async fetch)
|
||||
browser_page.wait_for_timeout(2000)
|
||||
|
||||
rows = browser_page.locator("#dashOrdersBody tr")
|
||||
count = rows.count()
|
||||
assert count > 0, "Expected at least 1 order row in dashboard table"
|
||||
|
||||
def test_orders_count_badges(self, browser_page: Page):
|
||||
"""Filter badges should show counts."""
|
||||
all_count = browser_page.locator("#dashCountAll").text_content()
|
||||
assert all_count and int(all_count) > 0, f"Expected total count > 0, got: {all_count}"
|
||||
|
||||
def test_first_order_has_columns(self, browser_page: Page):
|
||||
"""First row should have order number, date, customer, etc."""
|
||||
first_row = browser_page.locator("#dashOrdersBody tr").first
|
||||
cells = first_row.locator("td")
|
||||
assert cells.count() >= 6, f"Expected at least 6 columns, got: {cells.count()}"
|
||||
|
||||
# Order number should be a code element
|
||||
order_code = first_row.locator("td code").first
|
||||
expect(order_code).to_be_visible()
|
||||
|
||||
def test_filter_imported(self, browser_page: Page):
|
||||
"""Click 'Importate' filter and verify table updates."""
|
||||
browser_page.locator("#dashFilterBtns button", has_text="Importate").click()
|
||||
browser_page.wait_for_timeout(1000)
|
||||
|
||||
imported_count = browser_page.locator("#dashCountImported").text_content()
|
||||
if imported_count and int(imported_count) > 0:
|
||||
rows = browser_page.locator("#dashOrdersBody tr")
|
||||
assert rows.count() > 0, "Expected imported orders to show"
|
||||
# All visible rows should have 'Importat' badge
|
||||
badges = browser_page.locator("#dashOrdersBody .badge.bg-success")
|
||||
assert badges.count() > 0, "Expected green 'Importat' badges"
|
||||
|
||||
def test_filter_all_reset(self, browser_page: Page):
|
||||
"""Click 'Toate' to reset filter."""
|
||||
browser_page.locator("#dashFilterBtns button", has_text="Toate").click()
|
||||
browser_page.wait_for_timeout(1000)
|
||||
rows = browser_page.locator("#dashOrdersBody tr")
|
||||
assert rows.count() > 0, "Expected orders after resetting filter"
|
||||
|
||||
|
||||
class TestDashboardOrderDetail:
|
||||
"""Verify order detail modal opens and shows data."""
|
||||
|
||||
def test_click_order_opens_modal(self, browser_page: Page):
|
||||
browser_page.goto(f"{BASE_URL}/")
|
||||
browser_page.wait_for_load_state("networkidle")
|
||||
browser_page.wait_for_timeout(2000)
|
||||
|
||||
# Click the first order row
|
||||
first_row = browser_page.locator("#dashOrdersBody tr").first
|
||||
first_row.click()
|
||||
browser_page.wait_for_timeout(1500)
|
||||
|
||||
# Modal should be visible
|
||||
modal = browser_page.locator("#orderDetailModal")
|
||||
expect(modal).to_be_visible()
|
||||
|
||||
# Order number should be populated
|
||||
order_num = browser_page.locator("#detailOrderNumber").text_content()
|
||||
assert order_num and order_num != "#", f"Expected order number in modal, got: {order_num}"
|
||||
|
||||
def test_modal_shows_customer(self, browser_page: Page):
|
||||
customer = browser_page.locator("#detailCustomer").text_content()
|
||||
assert customer and customer not in ("...", "-"), f"Expected customer name, got: {customer}"
|
||||
|
||||
def test_modal_shows_items(self, browser_page: Page):
|
||||
items_rows = browser_page.locator("#detailItemsBody tr")
|
||||
assert items_rows.count() > 0, "Expected at least 1 item in order detail"
|
||||
|
||||
def test_close_modal(self, browser_page: Page):
|
||||
browser_page.locator("#orderDetailModal .btn-close").click()
|
||||
browser_page.wait_for_timeout(500)
|
||||
|
||||
|
||||
class TestDashboardAPIEndpoints:
|
||||
"""Verify API endpoints return expected data."""
|
||||
|
||||
def test_api_dashboard_orders(self, browser_page: Page):
|
||||
response = browser_page.request.get(f"{BASE_URL}/api/dashboard/orders")
|
||||
assert response.ok, f"API returned {response.status}"
|
||||
data = response.json()
|
||||
assert "orders" in data, "Expected 'orders' key in response"
|
||||
assert "counts" in data, "Expected 'counts' key in response"
|
||||
assert len(data["orders"]) > 0, "Expected at least 1 order"
|
||||
|
||||
def test_api_sync_status(self, browser_page: Page):
|
||||
response = browser_page.request.get(f"{BASE_URL}/api/sync/status")
|
||||
assert response.ok
|
||||
data = response.json()
|
||||
assert "status" in data
|
||||
assert "stats" in data
|
||||
|
||||
def test_api_sync_history(self, browser_page: Page):
|
||||
response = browser_page.request.get(f"{BASE_URL}/api/sync/history?per_page=5")
|
||||
assert response.ok
|
||||
data = response.json()
|
||||
assert "runs" in data
|
||||
assert len(data["runs"]) > 0, "Expected at least 1 sync run"
|
||||
|
||||
def test_api_missing_skus(self, browser_page: Page):
|
||||
response = browser_page.request.get(f"{BASE_URL}/api/validate/missing-skus")
|
||||
assert response.ok
|
||||
data = response.json()
|
||||
assert "missing_skus" in data
|
||||
57
api/tests/e2e/test_logs_filtering.py
Normal file
57
api/tests/e2e/test_logs_filtering.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""E2E: Logs page with per-order filtering and date display."""
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def navigate_to_logs(page: Page, app_url: str):
|
||||
page.goto(f"{app_url}/logs")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
|
||||
def test_logs_page_loads(page: Page):
|
||||
"""Verify the logs page renders with sync runs table."""
|
||||
expect(page.locator("h4")).to_contain_text("Jurnale Import")
|
||||
expect(page.locator("#runsTableBody")).to_be_visible()
|
||||
|
||||
|
||||
def test_sync_runs_table_headers(page: Page):
|
||||
"""Verify table has correct column headers."""
|
||||
headers = page.locator("thead th")
|
||||
texts = headers.all_text_contents()
|
||||
assert "Data" in texts, f"Expected 'Data' header, got: {texts}"
|
||||
assert "Status" in texts, f"Expected 'Status' header, got: {texts}"
|
||||
assert "Comenzi" in texts, f"Expected 'Comenzi' header, got: {texts}"
|
||||
|
||||
|
||||
def test_filter_buttons_exist(page: Page):
|
||||
"""Verify the log viewer section is initially hidden (no run selected yet)."""
|
||||
viewer = page.locator("#logViewerSection")
|
||||
expect(viewer).to_be_hidden()
|
||||
|
||||
|
||||
def test_order_detail_modal_structure(page: Page):
|
||||
"""Verify the order detail modal exists in DOM with required fields."""
|
||||
modal = page.locator("#orderDetailModal")
|
||||
expect(modal).to_be_attached()
|
||||
expect(page.locator("#detailOrderNumber")).to_be_attached()
|
||||
expect(page.locator("#detailCustomer")).to_be_attached()
|
||||
expect(page.locator("#detailDate")).to_be_attached()
|
||||
expect(page.locator("#detailItemsBody")).to_be_attached()
|
||||
|
||||
|
||||
def test_quick_map_modal_structure(page: Page):
|
||||
"""Verify quick map modal exists with multi-CODMAT support."""
|
||||
modal = page.locator("#quickMapModal")
|
||||
expect(modal).to_be_attached()
|
||||
expect(page.locator("#qmSku")).to_be_attached()
|
||||
expect(page.locator("#qmProductName")).to_be_attached()
|
||||
expect(page.locator("#qmCodmatLines")).to_be_attached()
|
||||
|
||||
|
||||
def test_text_log_toggle(page: Page):
|
||||
"""Verify text log section is hidden initially and toggle button is in DOM."""
|
||||
section = page.locator("#textLogSection")
|
||||
expect(section).to_be_hidden()
|
||||
# Toggle button lives inside logViewerSection which is also hidden
|
||||
expect(page.locator("#btnShowTextLog")).to_be_attached()
|
||||
81
api/tests/e2e/test_mappings.py
Normal file
81
api/tests/e2e/test_mappings.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""E2E: Mappings page with sortable headers, grouping, multi-CODMAT modal."""
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def navigate_to_mappings(page: Page, app_url: str):
|
||||
page.goto(f"{app_url}/mappings")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
|
||||
def test_mappings_page_loads(page: Page):
|
||||
"""Verify mappings page renders."""
|
||||
expect(page.locator("h4")).to_contain_text("Mapari SKU")
|
||||
|
||||
|
||||
def test_sortable_headers_present(page: Page):
|
||||
"""R7: Verify sortable column headers with sort icons."""
|
||||
sortable_ths = page.locator("th.sortable")
|
||||
count = sortable_ths.count()
|
||||
assert count >= 5, f"Expected at least 5 sortable columns, got {count}"
|
||||
|
||||
sort_icons = page.locator(".sort-icon")
|
||||
assert sort_icons.count() >= 5, f"Expected at least 5 sort-icon spans, got {sort_icons.count()}"
|
||||
|
||||
|
||||
def test_product_name_column_exists(page: Page):
|
||||
"""R4: Verify 'Produs Web' column exists in header."""
|
||||
headers = page.locator("thead th")
|
||||
texts = headers.all_text_contents()
|
||||
assert any("Produs Web" in t for t in texts), f"'Produs Web' column not found in headers: {texts}"
|
||||
|
||||
|
||||
def test_um_column_exists(page: Page):
|
||||
"""R12: Verify 'UM' column exists in header."""
|
||||
headers = page.locator("thead th")
|
||||
texts = headers.all_text_contents()
|
||||
assert any("UM" in t for t in texts), f"'UM' column not found in headers: {texts}"
|
||||
|
||||
|
||||
def test_show_inactive_toggle_exists(page: Page):
|
||||
"""R5: Verify 'Arata inactive' toggle is present."""
|
||||
toggle = page.locator("#showInactive")
|
||||
expect(toggle).to_be_visible()
|
||||
label = page.locator("label[for='showInactive']")
|
||||
expect(label).to_contain_text("Arata inactive")
|
||||
|
||||
|
||||
def test_sort_click_changes_icon(page: Page):
|
||||
"""R7: Clicking a sortable header should display a sort direction arrow."""
|
||||
sku_header = page.locator("th.sortable", has_text="SKU")
|
||||
sku_header.click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
icon = page.locator(".sort-icon[data-col='sku']")
|
||||
text = icon.text_content()
|
||||
assert text in ("↑", "↓"), f"Expected sort arrow (↑ or ↓), got '{text}'"
|
||||
|
||||
|
||||
def test_add_modal_multi_codmat(page: Page):
|
||||
"""R11: Verify the add mapping modal supports multiple CODMAT lines."""
|
||||
page.locator("button", has_text="Adauga Mapare").click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
codmat_lines = page.locator(".codmat-line")
|
||||
assert codmat_lines.count() >= 1, "Expected at least one CODMAT line in modal"
|
||||
|
||||
page.locator("button", has_text="Adauga CODMAT").click()
|
||||
page.wait_for_timeout(300)
|
||||
assert codmat_lines.count() >= 2, "Expected a second CODMAT line after clicking Adauga CODMAT"
|
||||
|
||||
# Second line must have a remove button
|
||||
remove_btns = page.locator(".codmat-line:nth-child(2) button.btn-outline-danger")
|
||||
assert remove_btns.count() >= 1, "Second CODMAT line is missing remove button"
|
||||
|
||||
|
||||
def test_search_input_exists(page: Page):
|
||||
"""Verify search input is present with the correct placeholder."""
|
||||
search = page.locator("#searchInput")
|
||||
expect(search).to_be_visible()
|
||||
expect(search).to_have_attribute("placeholder", "Cauta SKU, CODMAT sau denumire...")
|
||||
68
api/tests/e2e/test_missing_skus.py
Normal file
68
api/tests/e2e/test_missing_skus.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""E2E: Missing SKUs page with resolved toggle and multi-CODMAT modal."""
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def navigate_to_missing(page: Page, app_url: str):
|
||||
page.goto(f"{app_url}/missing-skus")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
|
||||
def test_missing_skus_page_loads(page: Page):
|
||||
"""Verify the page renders with the correct heading."""
|
||||
expect(page.locator("h4")).to_contain_text("SKU-uri Lipsa")
|
||||
|
||||
|
||||
def test_resolved_toggle_buttons(page: Page):
|
||||
"""R10: Verify resolved filter buttons exist and Nerezolvate is active by default."""
|
||||
expect(page.locator("#btnUnresolved")).to_be_visible()
|
||||
expect(page.locator("#btnResolved")).to_be_visible()
|
||||
expect(page.locator("#btnAll")).to_be_visible()
|
||||
|
||||
classes = page.locator("#btnUnresolved").get_attribute("class")
|
||||
assert "btn-primary" in classes, f"Expected #btnUnresolved to be active (btn-primary), got classes: {classes}"
|
||||
|
||||
|
||||
def test_resolved_toggle_switches(page: Page):
|
||||
"""R10: Clicking resolved/all toggles changes active state correctly."""
|
||||
# Click "Rezolvate"
|
||||
page.locator("#btnResolved").click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
classes_res = page.locator("#btnResolved").get_attribute("class")
|
||||
assert "btn-success" in classes_res, f"Expected #btnResolved to be active (btn-success), got: {classes_res}"
|
||||
|
||||
classes_unr = page.locator("#btnUnresolved").get_attribute("class")
|
||||
assert "btn-outline" in classes_unr, f"Expected #btnUnresolved to be outline after deactivation, got: {classes_unr}"
|
||||
|
||||
# Click "Toate"
|
||||
page.locator("#btnAll").click()
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
classes_all = page.locator("#btnAll").get_attribute("class")
|
||||
assert "btn-secondary" in classes_all, f"Expected #btnAll to be active (btn-secondary), got: {classes_all}"
|
||||
|
||||
|
||||
def test_map_modal_multi_codmat(page: Page):
|
||||
"""R11: Verify the mapping modal supports multiple CODMATs."""
|
||||
modal = page.locator("#mapModal")
|
||||
expect(modal).to_be_attached()
|
||||
|
||||
add_btn = page.locator("#mapModal button", has_text="Adauga CODMAT")
|
||||
expect(add_btn).to_be_attached()
|
||||
|
||||
expect(page.locator("#mapProductName")).to_be_attached()
|
||||
expect(page.locator("#mapPctWarning")).to_be_attached()
|
||||
|
||||
|
||||
def test_export_csv_button(page: Page):
|
||||
"""Verify Export CSV button is visible on the page."""
|
||||
btn = page.locator("button", has_text="Export CSV")
|
||||
expect(btn).to_be_visible()
|
||||
|
||||
|
||||
def test_rescan_button(page: Page):
|
||||
"""Verify Re-Scan button is visible on the page."""
|
||||
btn = page.locator("button", has_text="Re-Scan")
|
||||
expect(btn).to_be_visible()
|
||||
52
api/tests/e2e/test_order_detail.py
Normal file
52
api/tests/e2e/test_order_detail.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""E2E: Order detail modal structure and inline mapping."""
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
def test_order_detail_modal_has_roa_ids(page: Page, app_url: str):
|
||||
"""R9: Verify order detail modal contains all ROA ID labels."""
|
||||
page.goto(f"{app_url}/logs")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
modal = page.locator("#orderDetailModal")
|
||||
expect(modal).to_be_attached()
|
||||
|
||||
modal_html = modal.inner_html()
|
||||
assert "ID Comanda ROA" in modal_html, "Missing 'ID Comanda ROA' label in order detail modal"
|
||||
assert "ID Partener" in modal_html, "Missing 'ID Partener' label in order detail modal"
|
||||
assert "ID Adr. Facturare" in modal_html, "Missing 'ID Adr. Facturare' label in order detail modal"
|
||||
assert "ID Adr. Livrare" in modal_html, "Missing 'ID Adr. Livrare' label in order detail modal"
|
||||
|
||||
|
||||
def test_order_detail_items_table_columns(page: Page, app_url: str):
|
||||
"""R9: Verify items table has all required columns."""
|
||||
page.goto(f"{app_url}/logs")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
headers = page.locator("#orderDetailModal thead th")
|
||||
texts = headers.all_text_contents()
|
||||
|
||||
required_columns = ["SKU", "Produs", "Cant.", "Pret", "TVA", "CODMAT", "Status", "Actiune"]
|
||||
for col in required_columns:
|
||||
assert col in texts, f"Column '{col}' missing from order detail items table. Found: {texts}"
|
||||
|
||||
|
||||
def test_quick_map_from_order_detail(page: Page, app_url: str):
|
||||
"""R9+R11: Verify quick map modal is reachable from order detail context."""
|
||||
page.goto(f"{app_url}/logs")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
modal = page.locator("#quickMapModal")
|
||||
expect(modal).to_be_attached()
|
||||
|
||||
expect(page.locator("#qmCodmatLines")).to_be_attached()
|
||||
expect(page.locator("#qmPctWarning")).to_be_attached()
|
||||
|
||||
|
||||
def test_dashboard_navigates_to_logs(page: Page, app_url: str):
|
||||
"""Verify the sidebar on the dashboard contains a link to the logs page."""
|
||||
page.goto(f"{app_url}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
logs_link = page.locator("a[href='/logs']")
|
||||
expect(logs_link).to_be_visible()
|
||||
Reference in New Issue
Block a user