woodshop/tests/test_bom_window.py

107 lines
3.9 KiB
Python

"""Offscreen tests for the BOM window's drag/drop path (no display needed —
QGraphicsScene is pure Qt). Guards the placement-id vs cut-item-id crash."""
import os
import pytest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
pytest.importorskip("PySide6")
from PySide6.QtWidgets import QApplication # noqa: E402
from woodshop.cutplan import find_placement # noqa: E402
from woodshop.gui.bom_window import BomWindow, _Piece # noqa: E402
from woodshop.gui.controller import Controller # noqa: E402
_app = QApplication.instance() or QApplication([])
def _pieces(w):
return sorted((it for it in w.scene.items() if isinstance(it, _Piece)),
key=lambda it: it.pos().x())
def test_drop_overlap_reverts_without_crashing(tmp_path):
c = Controller(str(tmp_path / "s.json"))
c.place("2x4", 30)
c.place("2x4", 30) # one stick, two pieces
w = BomWindow(c)
first, second = _pieces(w)[:2]
home = (second.sp_id, second.pos().x(), second.pos().y())
second.setPos(0, second.pos().y()) # drop on top of the first -> overlap
w._drop_piece(second, home) # must not raise (was StopIteration)
assert "revert" in w._status.text().lower()
def test_drop_onto_incompatible_stock_reverts(tmp_path):
c = Controller(str(tmp_path / "s.json"))
c.place("2x4", 24)
c.place("ply-3/4", 24, width_in=24)
w = BomWindow(c)
lumber = next(it for it in _pieces(w)
if not find_placement(w._plan, it.pid)[0].is_sheet)
sheet_y = next(y0 for y0, _y1, sp in w._rows if sp.is_sheet)
home = (lumber.sp_id, lumber.pos().x(), lumber.pos().y())
lumber.setPos(10, sheet_y + 5) # drag the 2x4 onto the plywood sheet
w._drop_piece(lumber, home) # must not raise
assert "can't go" in w._status.text()
def test_best_of_n_button_keeps_valid_plan(tmp_path):
from woodshop.cutplan import validate_cut_plan
c = Controller(str(tmp_path / "s.json"))
for ln in (50, 46, 40, 30):
c.place("2x4", ln)
w = BomWindow(c)
w._best_of_n() # no locks -> best of 100
assert validate_cut_plan(w._plan) == []
assert "best" in w._status.text().lower()
def test_cost_tab_renders_estimate(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
c = Controller(str(tmp_path / "s.json"))
for _ in range(3):
c.place("2x4", 40)
w = BomWindow(c)
text = w._cost_te.toPlainText()
assert "COST ESTIMATE" in text and "2x4" in text and "Total" in text
def test_edit_prices_persists_and_updates(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
from woodshop import prices as P
c = Controller(str(tmp_path / "s.json"))
c.place("2x4", 40)
w = BomWindow(c)
w._prices["2x4"] = 9.99 # simulate an edit accepted from the dialog
P.save_prices(w._prices)
w._cost_te.setPlainText(w._cost_text())
assert "$9.99" in w._cost_te.toPlainText()
assert P.load_prices()["2x4"] == 9.99
def test_best_of_n_with_lock_runs_and_validates(tmp_path):
from woodshop.cutplan import validate_cut_plan
c = Controller(str(tmp_path / "s.json"))
for ln in (50, 46, 40, 30):
c.place("2x4", ln)
w = BomWindow(c)
p = next(p for sp in w._plan.stock_pieces for p in sp.placements)
p.locked = True
w._best_of_n() # locked path: strategies + shuffles
assert validate_cut_plan(w._plan) == []
assert "locked" in w._status.text()
def test_valid_move_commits(tmp_path):
c = Controller(str(tmp_path / "s.json"))
c.place("2x4", 20)
c.place("2x4", 20)
w = BomWindow(c)
second = _pieces(w)[1]
home = (second.sp_id, second.pos().x(), second.pos().y())
second.setPos(50 * w._px, second.pos().y()) # slide it right, still clear
w._drop_piece(second, home)
assert "placed" in w._status.text().lower()