303 lines
12 KiB
Python
303 lines
12 KiB
Python
"""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"
|