Цену было невозможно набрать. В прошлой правке я стал вырезать пробелы из вводимого текста, чтобы принимались разделители разрядов, — но у поля цены суффикс « ₽» начинается с пробела. Каждое нажатие ломало суффикс, Qt отвергало символ, и в поле оставалась только первая цифра: набираешь «25,50», получаешь 2. Теперь текст на каждом нажатии не трогается вообще: сначала пробуем как есть, и только если Qt отверг именно из-за точки — повторяем с запятой. Второе: форма продажи подключала сигнал редактора строк раньше, чем создавала поле оплаты. Qt глотает исключения в слотах, поэтому вместо падения оплата просто молча не подставлялась. Третье: выделение в таблицах держалось за номер строки. Таблицы отсортированы, и после перерисовки под тем же номером оказывался другой объект — можно было удалить не тот товар. Теперь выделение восстанавливается по идентификатору. Тесты: настоящие нажатия клавиш через QTest вместо проверок validate(), которые этот баг пропустили, и перехват sys.excepthook, чтобы проглоченные Qt исключения считались провалом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""Ввод с клавиатуры и поведение таблиц.
|
||
|
||
Отдельный файл, потому что проверять надо именно нажатия клавиш через QTest.
|
||
Проверки уровня validate() однажды уже прошли зелёными, пока набрать цену
|
||
в поле было физически невозможно: у поля суффикс « ₽», и «очистка» текста
|
||
на каждом нажатии его ломала, из-за чего принимался только первый символ.
|
||
"""
|
||
|
||
import os
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
|
||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
||
from PySide6.QtTest import QTest # noqa: E402
|
||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||
|
||
from app import journal # noqa: E402
|
||
from app.ui import theme # noqa: E402
|
||
from app.ui import widgets as w # noqa: E402
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def qapp():
|
||
app = QApplication.instance() or QApplication([])
|
||
theme.apply(app)
|
||
return app
|
||
|
||
|
||
def type_in(spin, keys: str):
|
||
"""Набрать значение так, как это делает человек: выделить и напечатать."""
|
||
spin.show()
|
||
spin.setFocus()
|
||
spin.lineEdit().selectAll()
|
||
QTest.keyClicks(spin.lineEdit(), keys)
|
||
spin.interpretText()
|
||
return spin.value_decimal()
|
||
|
||
|
||
# --- деньги ---------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"keys,expected",
|
||
[
|
||
("25", "25.00"),
|
||
("25,50", "25.50"),
|
||
("25.50", "25.50"),
|
||
("1200", "1200.00"),
|
||
("1234,56", "1234.56"),
|
||
("0,5", "0.50"),
|
||
("0.5", "0.50"),
|
||
],
|
||
)
|
||
def test_price_can_actually_be_typed(qapp, keys, expected):
|
||
"""Каждый символ должен доходить до значения, а не только первый."""
|
||
spin = w.MoneySpin()
|
||
spin.set_decimal(20)
|
||
assert type_in(spin, keys) == Decimal(expected)
|
||
spin.deleteLater()
|
||
|
||
|
||
def test_currency_suffix_survives_typing(qapp):
|
||
spin = w.MoneySpin()
|
||
spin.set_decimal(20)
|
||
type_in(spin, "137,40")
|
||
assert spin.lineEdit().text().endswith("₽")
|
||
assert spin.value_decimal() == Decimal("137.40")
|
||
spin.deleteLater()
|
||
|
||
|
||
def test_price_without_currency_also_works(qapp):
|
||
spin = w.MoneySpin(currency="")
|
||
spin.set_decimal(0)
|
||
assert type_in(spin, "99.99") == Decimal("99.99")
|
||
spin.deleteLater()
|
||
|
||
|
||
# --- количества -----------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"keys,expected",
|
||
[("3", "3.000"), ("0,5", "0.500"), ("0.5", "0.500"), ("1,125", "1.125"), ("1200", "1200.000")],
|
||
)
|
||
def test_quantity_can_actually_be_typed(qapp, keys, expected):
|
||
spin = w.QtySpin()
|
||
spin.set_decimal(1)
|
||
assert type_in(spin, keys) == Decimal(expected)
|
||
spin.deleteLater()
|
||
|
||
|
||
def test_typing_over_an_existing_value_replaces_it(qapp):
|
||
"""Выделил всё, напечатал новое — старое не должно примешиваться."""
|
||
spin = w.MoneySpin()
|
||
spin.set_decimal(1999)
|
||
assert type_in(spin, "7") == Decimal("7.00")
|
||
spin.deleteLater()
|
||
|
||
|
||
# --- выделение в таблицах -------------------------------------------------
|
||
|
||
|
||
def rows_for(names):
|
||
return [[w.text_item(name)] for name in names]
|
||
|
||
|
||
def test_selection_follows_the_object_not_the_row(qapp):
|
||
"""Таблицы отсортированы: под тем же номером после перерисовки может
|
||
оказаться другой объект, и «Удалить» сработает не на том."""
|
||
table = w.table(["Название"])
|
||
w.fill(table, rows_for(["Булка", "Ватрушка"]), keys=["b", "v"])
|
||
w.select_by_key(table, "v")
|
||
assert w.selected_key(table) == "v"
|
||
|
||
# Появился товар, встающий при сортировке первым.
|
||
w.fill(table, rows_for(["Ананас", "Булка", "Ватрушка"]), keys=["a", "b", "v"])
|
||
assert w.selected_key(table) == "v"
|
||
table.deleteLater()
|
||
|
||
|
||
def test_selection_survives_a_plain_refresh(qapp):
|
||
table = w.table(["Название"])
|
||
w.fill(table, rows_for(["Булка", "Ватрушка"]), keys=["b", "v"])
|
||
w.select_by_key(table, "b")
|
||
w.fill(table, rows_for(["Булка", "Ватрушка"]), keys=["b", "v"])
|
||
assert w.selected_key(table) == "b"
|
||
table.deleteLater()
|
||
|
||
|
||
def test_selection_is_dropped_when_the_object_is_gone(qapp):
|
||
table = w.table(["Название"])
|
||
w.fill(table, rows_for(["Булка", "Ватрушка"]), keys=["b", "v"])
|
||
w.select_by_key(table, "v")
|
||
w.fill(table, rows_for(["Булка"]), keys=["b"])
|
||
assert w.selected_key(table) != "v"
|
||
table.deleteLater()
|
||
|
||
|
||
# --- правка цены целиком, как это делает пользователь ---------------------
|
||
|
||
|
||
def test_price_edit_reaches_the_product(qapp, vault, buns):
|
||
"""Сквозной путь: набрать цены в диалоге и увидеть их в товаре."""
|
||
from app.ui.page_products import PriceDialog
|
||
|
||
product = buns["мак"]
|
||
dialog = PriceDialog("₽", product)
|
||
assert type_in(dialog.cost, "25,50") == Decimal("25.50")
|
||
assert type_in(dialog.retail, "44.90") == Decimal("44.90")
|
||
|
||
journal.change_prices(
|
||
vault, product.id, dialog.cost.value_decimal(), dialog.retail.value_decimal()
|
||
)
|
||
updated = vault.doc.product(product.id)
|
||
assert updated.cost_price == Decimal("25.50")
|
||
assert updated.retail_price == Decimal("44.90")
|
||
assert len(updated.price_history) == 2
|
||
dialog.deleteLater()
|