food-market/tests/test_dialogs.py
Claude 4dd9a071c0 Починен ввод цены и выделение в таблицах
Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы
из вводимого текста, чтобы принимались разделители разрядов, — но у поля
цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс,
Qt отвергало символ, и в поле оставалась только первая цифра: набираешь
«25,50», получаешь 2.

Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как
есть, и только если Qt отверг именно из-за точки — повторяем с запятой.

Второе: форма продажи подключала сигнал редактора строк раньше, чем
создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо
падения оплата просто молча не подставлялась.

Третье: выделение в таблицах держалось за номер строки. Таблицы
отсортированы, и после перерисовки под тем же номером оказывался другой
объект — можно было удалить не тот товар. Теперь выделение
восстанавливается по идентификатору.

Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(),
которые этот баг пропустили, и перехват sys.excepthook, чтобы
проглоченные Qt исключения считались провалом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:13:45 +03:00

186 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Диалоги должны собираться без единого проглоченного исключения.
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()