"""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"