Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы из вводимого текста, чтобы принимались разделители разрядов, — но у поля цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс, Qt отвергало символ, и в поле оставалась только первая цифра: набираешь «25,50», получаешь 2. Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как есть, и только если Qt отверг именно из-за точки — повторяем с запятой. Второе: форма продажи подключала сигнал редактора строк раньше, чем создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо падения оплата просто молча не подставлялась. Третье: выделение в таблицах держалось за номер строки. Таблицы отсортированы, и после перерисовки под тем же номером оказывался другой объект — можно было удалить не тот товар. Теперь выделение восстанавливается по идентификатору. Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(), которые этот баг пропустили, и перехват sys.excepthook, чтобы проглоченные Qt исключения считались провалом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
5.9 KiB
Python
186 lines
5.9 KiB
Python
"""Диалоги должны собираться без единого проглоченного исключения.
|
||
|
||
Qt не роняет приложение, когда слот падает: он печатает traceback и живёт
|
||
дальше. Внешне это выглядит как «работает, но криво» — например, у формы
|
||
продажи однажды не подставлялась оплата, потому что сигнал редактора строк
|
||
приходил раньше, чем создавалось поле оплаты.
|
||
|
||
Поэтому здесь перехватывается sys.excepthook, а не просто проверяется, что
|
||
код не бросил исключение наружу.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from datetime import date, timedelta
|
||
|
||
import pytest
|
||
|
||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||
|
||
from app import journal, ledger # noqa: E402
|
||
from app.models import KIND_RETAIL, KIND_SELF # noqa: E402
|
||
from app.ui import theme # noqa: E402
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def qapp():
|
||
app = QApplication.instance() or QApplication([])
|
||
theme.apply(app)
|
||
return app
|
||
|
||
|
||
@pytest.fixture
|
||
def silent():
|
||
"""Собрать всё, что Qt проглотил бы, напечатав в stderr."""
|
||
caught = []
|
||
|
||
def hook(exc_type, exc, tb):
|
||
caught.append(f"{exc_type.__name__}: {exc}")
|
||
|
||
previous = sys.excepthook
|
||
sys.excepthook = hook
|
||
try:
|
||
yield caught
|
||
finally:
|
||
sys.excepthook = previous
|
||
|
||
|
||
class Ctx:
|
||
def __init__(self, vault):
|
||
self.vault = vault
|
||
self.report = ledger.build(vault.doc)
|
||
|
||
@property
|
||
def currency(self):
|
||
return self.vault.doc.settings.currency
|
||
|
||
def changed(self):
|
||
self.report = ledger.build(self.vault.doc)
|
||
|
||
def sync_now(self, quiet=False):
|
||
pass
|
||
|
||
def sync_status_text(self):
|
||
return ""
|
||
|
||
def restart_sync_timer(self):
|
||
pass
|
||
|
||
conflict_message = None
|
||
|
||
|
||
@pytest.fixture
|
||
def ctx(qapp, vault, buns):
|
||
today = date.today()
|
||
journal.create_batch(
|
||
vault, today - timedelta(days=3), today + timedelta(days=11),
|
||
[{"product_id": buns["мак"].id, "qty": 20, "unit_cost": 20}],
|
||
)
|
||
vasya = journal.create_counterparty(vault, "Вася")
|
||
journal.create_sale(
|
||
vault, today, KIND_RETAIL, [{"product_id": buns["мак"].id, "qty": 2, "unit_price": 35}],
|
||
counterparty_id=vasya.id, paid_amount=0,
|
||
)
|
||
journal.create_sale(
|
||
vault, today, KIND_SELF, [{"product_id": buns["повидло"].id, "qty": 1, "unit_price": 0}]
|
||
)
|
||
return Ctx(vault)
|
||
|
||
|
||
def test_new_sale_dialog_is_quiet(ctx, silent):
|
||
from app.ui.page_sales import SaleDialog
|
||
|
||
dialog = SaleDialog(ctx)
|
||
assert silent == []
|
||
# Оплата должна подставиться сразу — ради этого сигнал и существует.
|
||
assert dialog.paid.value_decimal() == dialog.lines.total()
|
||
assert dialog.paid_hint.text()
|
||
dialog.deleteLater()
|
||
|
||
|
||
def test_existing_sale_dialog_is_quiet(ctx, silent):
|
||
from app.ui.page_sales import SaleDialog
|
||
|
||
sale = ctx.vault.doc.sales[0]
|
||
dialog = SaleDialog(ctx, sale)
|
||
assert silent == []
|
||
# У существующей продажи платежи правятся на экране долгов.
|
||
assert dialog.paid is None
|
||
dialog.deleteLater()
|
||
|
||
|
||
def test_switching_kind_is_quiet(ctx, silent):
|
||
from app.ui.page_sales import SaleDialog
|
||
from app.models import SALE_KINDS
|
||
|
||
dialog = SaleDialog(ctx)
|
||
for index in range(len(SALE_KINDS)):
|
||
dialog.kind.setCurrentIndex(index)
|
||
assert silent == []
|
||
dialog.deleteLater()
|
||
|
||
|
||
def test_batch_dialogs_are_quiet(ctx, silent):
|
||
from app.ui.page_batches import BatchDialog, PaymentDialog
|
||
|
||
new = BatchDialog(ctx)
|
||
new.date.set_date(date.today() - timedelta(days=7)) # двигает дедлайн
|
||
existing = BatchDialog(ctx, ctx.vault.doc.batches[0])
|
||
payment = PaymentDialog(ctx, ctx.vault.doc.batches[0], 100)
|
||
assert silent == []
|
||
for d in (new, existing, payment):
|
||
d.deleteLater()
|
||
|
||
|
||
def test_product_dialogs_are_quiet(ctx, silent):
|
||
from app.ui.page_products import PriceDialog, ProductDialog
|
||
|
||
product = ctx.vault.doc.products[0]
|
||
creating = ProductDialog("₽")
|
||
creating.unit.setText("л") # пересобирает заголовок фасовок
|
||
creating.packs.add_pack("бутылка", "1.5", 170)
|
||
editing = ProductDialog("₽", product)
|
||
prices = PriceDialog("₽", product)
|
||
assert silent == []
|
||
for d in (creating, editing, prices):
|
||
d.deleteLater()
|
||
|
||
|
||
def test_quick_sales_dialog_is_quiet(ctx, silent):
|
||
from app.ui.quick_sales import QuickSalesDialog
|
||
|
||
dialog = QuickSalesDialog(ctx)
|
||
dialog.add_row()
|
||
dialog.table.setCurrentCell(0, 0)
|
||
dialog.duplicate_row()
|
||
dialog.period_from.set_date(date.today() - timedelta(days=60))
|
||
dialog.remove_row()
|
||
assert silent == []
|
||
dialog.deleteLater()
|
||
|
||
|
||
def test_payment_dialog_is_quiet(ctx, silent):
|
||
from app.ui.page_debts import PaymentDialog
|
||
|
||
dialog = PaymentDialog(ctx.report.summary.receivable, "₽")
|
||
assert silent == []
|
||
dialog.deleteLater()
|
||
|
||
|
||
def test_every_page_refreshes_quietly(ctx, silent):
|
||
from app.ui.page_batches import BatchesPage
|
||
from app.ui.page_dashboard import DashboardPage
|
||
from app.ui.page_debts import DebtsPage
|
||
from app.ui.page_journal import JournalPage
|
||
from app.ui.page_products import ProductsPage
|
||
from app.ui.page_sales import SalesPage
|
||
|
||
for cls in (DashboardPage, BatchesPage, SalesPage, DebtsPage, ProductsPage, JournalPage):
|
||
page = cls(ctx)
|
||
page.refresh()
|
||
page.refresh() # повторная перерисовка — там живёт восстановление выделения
|
||
assert silent == [], f"{cls.__name__}: {silent}"
|
||
page.deleteLater()
|