Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,302 @@
"""Playwright E2E: кормораздатчик — dashboard, copy/paste, unlink delete, move."""
from __future__ import annotations
import pytest
from tests.e2e.conftest import (
E2E_DISP_ID,
E2E_DISP_RECIPE_1,
E2E_DISP_RECIPE_2,
E2E_PERIOD_A,
E2E_PERIOD_B,
)
from tests.e2e.recipes_e2e_helpers import (
drag_reorder_recipe,
fetch_recipe,
list_dom_recipe_ids,
period_recipe_ids,
pointer_drag_reorder_recipe,
)
pytestmark = pytest.mark.e2e
SCENARIO_E2E_DRAG_REORDER = "E2E_DRAG_REORDER"
SCENARIO_E2E_MOVE_RECIPE_UP = "E2E_MOVE_RECIPE_UP"
SCENARIO_E2E_EDITOR_UNLOADING_GROUP = "E2E_EDITOR_UNLOADING_GROUP"
SCENARIO_E2E_EDITOR_CANCEL = "E2E_EDITOR_CANCEL"
def test_dispenser_dashboard_three_columns(authenticated_page) -> None:
page = authenticated_page
assert page.locator("#dispensersCol").is_visible()
page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_DISP_ID}"]').click()
page.wait_for_selector(
f'[data-action="select-period"][data-period-id="{E2E_PERIOD_A}"]', timeout=10000
)
assert page.locator("#periodsCol").is_visible()
assert page.locator("#recipesCol").count() == 1
def test_select_period_shows_recipes(two_periods_setup) -> None:
page = two_periods_setup
assert page.locator(
f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]'
).is_visible()
assert page.locator(
f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_2}"]'
).is_visible()
def test_create_recipe_opens_editor(two_periods_setup) -> None:
page = two_periods_setup
create_btn = page.locator('[data-action="create-recipe"]').first
create_btn.click()
page.wait_for_selector("#recipeEdit", state="visible", timeout=15000)
assert page.locator("#recipeName").is_visible()
def test_copy_paste_recipe_between_periods(two_periods_setup, live_server_url) -> None:
page = two_periods_setup
with page.expect_response(
lambda r: r.request.method == "GET" and f"/api/recipes/{E2E_DISP_RECIPE_1}" in r.url,
timeout=15000,
):
page.locator(
f'[data-action="copy-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]'
).first.click()
page.locator(f'[data-action="select-period"][data-period-id="{E2E_PERIOD_B}"]').click()
paste = page.locator(
f'[data-action="paste-recipe"][data-period-id="{E2E_PERIOD_B}"]'
)
page.wait_for_timeout(400)
assert not paste.is_disabled(), "paste должен быть активен после copy"
paste.click()
page.wait_for_selector("#recipeEdit", state="visible", timeout=15000)
assert "(копия)" in page.locator("#recipeName").input_value()
group_rows = page.locator("#unloadingGroupsTableBody .unloading-group-row")
page.wait_for_timeout(500)
assert group_rows.count() >= 1, "после paste должна остаться хотя бы одна группа выгрузки"
page.locator('[data-action="save-and-exit"]').click()
page.wait_for_selector("#recipeEdit", state="hidden", timeout=15000)
listed = page.request.get(f"{live_server_url}/api/periods/{E2E_PERIOD_B}/recipes")
assert listed.ok
new_ids = [r["id"] for r in listed.json() if r["id"] != E2E_DISP_RECIPE_1]
assert new_ids
pasted = fetch_recipe(page, live_server_url, new_ids[0])
groups = pasted.get("unloading_groups") or []
assert groups
assert groups[0].get("distribution_type") in ("heads", "percent")
assert float(groups[0].get("value") or 0) > 0
def test_heads_change_recalculates_group_weights(dispenser_recipe_open) -> None:
SCENARIO_E2E_HEADS_RECALC = "E2E_HEADS_RECALC" # noqa: F841
page = dispenser_recipe_open
type_select = page.locator(
"#unloadingGroupsTableBody select[data-action='group-type-change']"
).first
if type_select.count():
type_select.select_option("heads")
page.wait_for_timeout(400)
weight_input = page.locator("#unloadingGroupsTableBody .group-weight").first
page.wait_for_timeout(800)
before = float(weight_input.input_value() or 0)
with page.expect_response(
lambda r: "/api/recipes/calculate" in r.url and r.request.method == "POST",
timeout=15000,
):
page.locator("#headsCount").fill("5")
page.locator("#headsCount").dispatch_event("input")
page.locator("#headsCount").dispatch_event("change")
page.wait_for_timeout(500)
after = float(weight_input.input_value() or 0)
assert before > 0
assert after != before
def test_move_recipe_down_changes_dom_order(two_periods_setup) -> None:
page = two_periods_setup
items = page.locator("#recipesList .list-item[data-recipe-id]")
before = [items.nth(i).get_attribute("data-recipe-id") for i in range(items.count())]
assert before == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
with page.expect_response(
lambda r: r.request.method == "PUT" and "/move" in r.url,
timeout=15000,
):
page.locator(
f'[data-action="move-recipe-down"][data-recipe-id="{E2E_DISP_RECIPE_1}"]'
).click()
page.wait_for_timeout(600)
after = [items.nth(i).get_attribute("data-recipe-id") for i in range(items.count())]
assert after == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1]
def test_dispenser_editor_shows_ingredients(dispenser_recipe_open) -> None:
page = dispenser_recipe_open
rows = page.locator("#ingredientsTableBody .ingredient-row")
assert rows.count() >= 1
def test_move_recipe_up_button_present(two_periods_setup) -> None:
page = two_periods_setup
assert page.locator('[data-action="move-recipe-up"]').count() >= 1
assert page.locator('[data-action="move-recipe-down"]').count() >= 1
def test_recipe_drag_handle_present(two_periods_setup) -> None:
page = two_periods_setup
assert page.locator('[data-action="recipe-drag-handle"]').count() >= 1
def test_delete_recipe_unlinks_from_period(two_periods_setup, live_server_url) -> None:
page = two_periods_setup
page.on("dialog", lambda d: d.accept())
with page.expect_response(
lambda r: r.request.method == "DELETE"
and f"/periods/{E2E_PERIOD_A}/recipes/" in r.url,
timeout=15000,
):
page.locator(
f'[data-action="delete-recipe"][data-recipe-id="{E2E_DISP_RECIPE_2}"]'
).click()
page.wait_for_timeout(500)
assert (
page.locator(
f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_2}"]'
).count()
== 0
)
resp = page.request.get(f"{live_server_url}/api/recipes/{E2E_DISP_RECIPE_2}")
assert resp.ok
def test_drag_reorder_swaps_recipes_and_api_order(two_periods_setup, live_server_url) -> None:
SCENARIO_E2E_DRAG_REORDER # noqa: F841
page = two_periods_setup
page.set_viewport_size({"width": 1280, "height": 800})
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
with page.expect_response(
lambda r: r.request.method == "PUT" and "/move" in r.url,
timeout=15000,
):
drag_reorder_recipe(page, E2E_DISP_RECIPE_1, direction="down")
page.wait_for_timeout(500)
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1]
assert period_recipe_ids(page, live_server_url, E2E_PERIOD_A) == [
E2E_DISP_RECIPE_2,
E2E_DISP_RECIPE_1,
]
def test_mouse_drag_reorder_works_twice_in_a_row(two_periods_setup) -> None:
page = two_periods_setup
page.set_viewport_size({"width": 1280, "height": 800})
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
drag_reorder_recipe(page, E2E_DISP_RECIPE_1, direction="down")
page.wait_for_timeout(400)
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1]
drag_reorder_recipe(
page, E2E_DISP_RECIPE_1, direction="up", expect_move_response=False
)
page.wait_for_timeout(400)
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
def test_pointer_drag_reorder_works_twice_in_a_row(two_periods_setup) -> None:
page = two_periods_setup
page.set_viewport_size({"width": 1280, "height": 800})
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
first = pointer_drag_reorder_recipe(page, E2E_DISP_RECIPE_1, direction="down")
assert first.get("ok"), first
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1]
page.wait_for_timeout(400)
second = pointer_drag_reorder_recipe(page, E2E_DISP_RECIPE_1, direction="up")
assert second.get("ok"), second
assert second.get("reorderActive"), second
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
def test_pointer_drag_reorder_swaps_dom_order(two_periods_setup) -> None:
"""Pointer-path: ловит баг, когда transfer endSession рвёт активный reorder."""
page = two_periods_setup
page.set_viewport_size({"width": 1280, "height": 800})
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
result = pointer_drag_reorder_recipe(page, E2E_DISP_RECIPE_1, direction="down")
assert result.get("ok"), result
assert result.get("reorderActive"), "pointer reorder should activate"
assert result.get("idsAfter") == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], result
def test_move_recipe_up_changes_api_order(two_periods_setup, live_server_url) -> None:
SCENARIO_E2E_MOVE_RECIPE_UP # noqa: F841
page = two_periods_setup
page.locator(
f'[data-action="move-recipe-down"][data-recipe-id="{E2E_DISP_RECIPE_1}"]'
).click()
page.wait_for_timeout(500)
assert period_recipe_ids(page, live_server_url, E2E_PERIOD_A) == [
E2E_DISP_RECIPE_2,
E2E_DISP_RECIPE_1,
]
with page.expect_response(
lambda r: r.request.method == "PUT" and "/move" in r.url,
timeout=15000,
):
page.locator(
f'[data-action="move-recipe-up"][data-recipe-id="{E2E_DISP_RECIPE_1}"]'
).click()
page.wait_for_timeout(500)
assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2]
assert period_recipe_ids(page, live_server_url, E2E_PERIOD_A) == [
E2E_DISP_RECIPE_1,
E2E_DISP_RECIPE_2,
]
def test_editor_add_remove_unloading_group(dispenser_recipe_open) -> None:
SCENARIO_E2E_EDITOR_UNLOADING_GROUP # noqa: F841
page = dispenser_recipe_open
rows = page.locator("#unloadingGroupsTableBody .unloading-group-row")
before = rows.count()
assert before >= 1
page.locator('[data-action="add-unloading-group"]').click()
page.wait_for_timeout(300)
assert rows.count() == before + 1
page.locator('[data-action="remove-unloading-group"]').last.click()
page.wait_for_timeout(200)
assert rows.count() == before
def test_editor_cancel_discards_unsaved_name(dispenser_recipe_open) -> None:
SCENARIO_E2E_EDITOR_CANCEL # noqa: F841
page = dispenser_recipe_open
original = page.locator("#recipeName").input_value()
page.locator("#recipeName").fill("E2E Не должно сохраниться")
page.locator('[data-action="show-recipe-edit-off"]').first.click()
page.wait_for_timeout(400)
assert page.locator("#recipeEdit").is_hidden() or not page.locator("#recipeEdit").is_visible()
page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click()
page.wait_for_selector("#recipeEdit", state="visible", timeout=15000)
assert page.locator("#recipeName").input_value() == original
def test_editor_group_value_change_updates_input(dispenser_recipe_open) -> None:
page = dispenser_recipe_open
inp = page.locator(
"#unloadingGroupsTableBody .unloading-group-row [data-action='group-value-change']"
).first
assert inp.is_visible()
inp.fill("77.5")
assert inp.input_value() == "77.5"