128 lines
4.7 KiB
Python
128 lines
4.7 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 "PROJECT ESTIMATE" in text and "TOTAL COST" in text and "SUGGESTED PRICE" in text
|
||
|
||
|
||
def test_margin_spin_updates_price_and_persists(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
||
from woodshop import estimate as E
|
||
c = Controller(str(tmp_path / "s.json"))
|
||
c.place("2x4", 40)
|
||
w = BomWindow(c)
|
||
w._margin_spin.setValue(50.0) # fires _on_margin_changed
|
||
assert "50%" in w._cost_te.toPlainText()
|
||
assert E.load_rates().margin_pct == 50.0
|
||
|
||
|
||
def test_target_overrides_margin(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "cfg"))
|
||
c = Controller(str(tmp_path / "s.json"))
|
||
c.place("2x4", 40)
|
||
w = BomWindow(c)
|
||
w._target_spin.setValue(500.0)
|
||
assert "Target price" in w._cost_te.toPlainText()
|
||
assert "$500.00" in w._cost_te.toPlainText()
|
||
|
||
|
||
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"] = 10.00 # simulate an edit accepted from the dialog
|
||
P.save_prices(w._prices)
|
||
w._cost_te.setPlainText(w._cost_text())
|
||
assert "$11.50" in w._cost_te.toPlainText() # 1 stick × $10 + 15% HST
|
||
assert P.load_prices()["2x4"] == 10.00
|
||
|
||
|
||
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()
|