"""E2E user journeys: хаотичные длинные сценарии «полный еблан за терминалом».""" from __future__ import annotations import pytest from tests.e2e.conftest import ( E2E_DISP_ID, E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2, E2E_MILL_ID, E2E_PERIOD_A, E2E_PERIOD_B, E2E_RECIPE_ID, ) from tests.e2e.recipes_e2e_helpers import ( abort_transfer_via_escape, assert_period_order, click_copy_recipe, click_move_recipe, create_recipe_in_period, delete_recipe_unlink, drag_reorder_recipe, drag_transfer_to_period, ensure_two_recipe_order, fetch_recipe, list_dom_recipe_ids, open_dispenser_period, period_recipe_ids, recipe_ingredient_snapshot, restore_two_recipe_order_if_reversed, select_period, wait_editor_closed, wait_editor_open, wait_mobile_dashboard_step, wait_period_order, ) pytestmark = pytest.mark.e2e MOBILE_VIEWPORT = {"width": 390, "height": 844} E2E_DISP_ING_1 = "e2e-disp-ing-1" SCENARIO_JOURNEY_MORNING_ROUTINE = "JOURNEY_MORNING_ROUTINE" SCENARIO_JOURNEY_TRANSFER_REPEAT_SLOTS = "JOURNEY_TRANSFER_REPEAT_SLOTS" SCENARIO_JOURNEY_ESCAPE_REORDER_MIX = "JOURNEY_ESCAPE_REORDER_MIX" SCENARIO_JOURNEY_MILL_CREATE_DELETE = "JOURNEY_MILL_CREATE_DELETE" SCENARIO_JOURNEY_SETTINGS_LOGOUT = "JOURNEY_SETTINGS_LOGOUT" SCENARIO_JOURNEY_EMPTY_THEN_FULL = "JOURNEY_EMPTY_THEN_FULL" SCENARIO_JOURNEY_MOBILE_FLIP = "JOURNEY_MOBILE_FLIP" SCENARIO_JOURNEY_EDITOR_CANCEL_REOPEN = "JOURNEY_EDITOR_CANCEL_REOPEN" SCENARIO_JOURNEY_MOVE_BUTTONS_YOYO = "JOURNEY_MOVE_BUTTONS_YOYO" SCENARIO_JOURNEY_DOUBLE_COPY_PASTE = "JOURNEY_DOUBLE_COPY_PASTE" SCENARIO_JOURNEY_MILL_DISPENSER_PINGPONG = "JOURNEY_MILL_DISPENSER_PINGPONG" SCENARIO_JOURNEY_DISTRACTED_UI = "JOURNEY_DISTRACTED_UI" SCENARIO_JOURNEY_MEGA_MIX = "JOURNEY_MEGA_MIX" def _open_dispenser_period_a(page, live_server_url) -> None: open_dispenser_period( page, E2E_PERIOD_A, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2], ) def test_journey_morning_routine_save_reorder_copy_paste_unlink( authenticated_page, live_server_url ) -> None: SCENARIO_JOURNEY_MORNING_ROUTINE # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Утренний рейс") with page.expect_response( lambda r: r.request.method == "PUT" and E2E_DISP_RECIPE_1 in r.url, timeout=15000, ): page.locator('[data-action="save-and-exit"]').click() wait_editor_closed(page) assert fetch_recipe(page, live_server_url, E2E_DISP_RECIPE_1)["name"] == "E2E Утренний рейс" assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) _open_dispenser_period_a(page, live_server_url) drag_reorder_recipe( page, E2E_DISP_RECIPE_1, direction="down", live_server_url=live_server_url, period_id=E2E_PERIOD_A, expected_order=[E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], ) click_copy_recipe(page, E2E_DISP_RECIPE_1) select_period(page, E2E_PERIOD_B, live_server_url=live_server_url, expected_order=[]) page.locator(f'[data-action="paste-recipe"][data-period-id="{E2E_PERIOD_B}"]').click() wait_editor_open(page) with page.expect_response( lambda r: r.request.method == "POST" and f"/api/periods/{E2E_PERIOD_B}/recipes" in r.url, timeout=20000, ) as paste_save: page.locator('[data-action="save-and-exit"]').click() pasted_id = paste_save.value.json().get("id") assert pasted_id wait_editor_closed(page) wait_period_order(page, live_server_url, E2E_PERIOD_B, [pasted_id]) select_period(page, E2E_PERIOD_B, wait_recipe_id=pasted_id) delete_recipe_unlink( page, pasted_id, live_server_url=live_server_url, period_id=E2E_PERIOD_B, expected_period_order=[], ) assert page.request.get(f"{live_server_url}/api/recipes/{pasted_id}").ok def test_journey_transfer_repeat_with_slots(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_TRANSFER_REPEAT_SLOTS # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) drag_transfer_to_period( page, E2E_DISP_RECIPE_1, E2E_PERIOD_B, live_server_url=live_server_url, source_period_id=E2E_PERIOD_A, expected_source_order=[E2E_DISP_RECIPE_2], expected_target_order=[E2E_DISP_RECIPE_1], ) assert page.locator("button.recipe-transfer-slot").count() == 0 select_period( page, E2E_PERIOD_B, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1], ) drag_transfer_to_period( page, E2E_DISP_RECIPE_1, E2E_PERIOD_A, slot_index=1, live_server_url=live_server_url, source_period_id=E2E_PERIOD_B, expected_source_order=[], expected_target_order=[E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], ) def test_journey_escape_then_reorder_still_works(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_ESCAPE_REORDER_MIX # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) abort_transfer_via_escape(page, E2E_DISP_RECIPE_1) assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) drag_reorder_recipe( page, E2E_DISP_RECIPE_1, direction="down", live_server_url=live_server_url, period_id=E2E_PERIOD_A, expected_order=[E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], ) assert list_dom_recipe_ids(page) == [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] def test_journey_mill_create_edit_delete_global(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_MILL_CREATE_DELETE # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.locator("#createRecipeBtn").click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Journey Mill") with page.expect_response( lambda r: r.request.method == "POST" and r.url.rstrip("/").endswith("/api/recipes"), timeout=20000, ) as resp_info: page.locator('[data-action="save-and-exit"]').click() recipe_id = resp_info.value.json().get("id") assert recipe_id wait_editor_closed(page) assert fetch_recipe(page, live_server_url, recipe_id)["name"] == "E2E Journey Mill" page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.locator(f'[data-action="select-recipe"][data-recipe-id="{recipe_id}"]').click() wait_editor_open(page) before_ings = len(fetch_recipe(page, live_server_url, recipe_id).get("ingredients") or []) page.locator('[data-action="add-ingredient"]').click() page.wait_for_function( f"() => document.querySelectorAll('#ingredientsTableBody .zt-data-grid__row').length > {before_ings}" ) page.locator('[data-action="remove-ingredient"]').first.click() with page.expect_response( lambda r: r.request.method == "PUT" and recipe_id in r.url, timeout=15000, ): page.locator('[data-action="save-and-exit"]').click() wait_editor_closed(page) assert len(fetch_recipe(page, live_server_url, recipe_id).get("ingredients") or []) == before_ings page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.on("dialog", lambda d: d.accept()) with page.expect_response( lambda r: r.request.method == "DELETE" and recipe_id in r.url, timeout=15000, ): page.locator(f'[data-action="delete-recipe"][data-recipe-id="{recipe_id}"]').click() assert page.request.get(f"{live_server_url}/api/recipes/{recipe_id}").status == 404 assert page.request.get(f"{live_server_url}/api/recipes/{E2E_RECIPE_ID}").ok def test_journey_settings_then_logout(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_SETTINGS_LOGOUT # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) page.locator('[data-action="open-settings"]').first.click(force=True) page.wait_for_selector('[data-action="toggle-sync-form"]', timeout=5000) page.locator('[data-action="toggle-sync-form"]').click() page.locator('[data-action="toggle-credentials-form"]').click() page.locator('#settingsModal [data-action="close-settings"]').first.click() page.wait_for_selector('[data-action="open-settings"]', state="visible", timeout=5000) with page.expect_response( lambda r: r.url.endswith("/api/auth/logout") and r.request.method == "POST", timeout=10000, ): page.locator('[data-action="logout"]').first.click() page.wait_for_url(f"{live_server_url}/", timeout=10000) check = page.request.get(f"{live_server_url}/api/auth/check") assert check.ok assert check.json().get("authenticated") is False def test_journey_empty_period_create_then_delete_all(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_EMPTY_THEN_FULL # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_DISP_ID}"]').click() select_period( page, E2E_PERIOD_B, live_server_url=live_server_url, expected_order=[], ) created_ids: list[str] = [] for i in range(2): page.wait_for_selector( f'[data-action="create-recipe"][data-period-id="{E2E_PERIOD_B}"]', state="visible", timeout=10000, ) rid = create_recipe_in_period( page, E2E_PERIOD_B, f"E2E Пустой B {i}", live_server_url=live_server_url, ) created_ids.append(rid) wait_period_order(page, live_server_url, E2E_PERIOD_B, created_ids.copy()) assert len(period_recipe_ids(page, live_server_url, E2E_PERIOD_B)) == 2 for rid in created_ids: select_period(page, E2E_PERIOD_B, wait_recipe_id=rid) remaining = [x for x in created_ids if x != rid] delete_recipe_unlink( page, rid, live_server_url=live_server_url, period_id=E2E_PERIOD_B, expected_period_order=remaining, ) assert_period_order(page, live_server_url, E2E_PERIOD_B, []) def test_journey_mobile_flip_back_and_button_reorder( authenticated_page, live_server_url ) -> None: SCENARIO_JOURNEY_MOBILE_FLIP # noqa: F841 page = authenticated_page page.set_viewport_size(MOBILE_VIEWPORT) open_dispenser_period( page, E2E_PERIOD_A, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2], ) page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click() wait_editor_open(page) page.locator('[data-action="show-recipe-edit-off"]').first.click() wait_editor_closed(page) page.locator('[data-action="mobile-dashboard-back"]').click() wait_mobile_dashboard_step(page, "periods") select_period( page, E2E_PERIOD_A, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2], ) click_move_recipe(page, E2E_DISP_RECIPE_1, "down") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] ) page.locator('[data-action="mobile-dashboard-back"]').click() wait_mobile_dashboard_step(page, "periods") select_period( page, E2E_PERIOD_B, live_server_url=live_server_url, expected_order=[], ) page.locator('[data-action="mobile-dashboard-back"]').click() wait_mobile_dashboard_step(page, "periods") select_period( page, E2E_PERIOD_A, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], ) click_move_recipe(page, E2E_DISP_RECIPE_1, "up") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) def test_journey_editor_cancel_then_save_other_recipe( authenticated_page, live_server_url ) -> None: SCENARIO_JOURNEY_EDITOR_CANCEL_REOPEN # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) before_name = fetch_recipe(page, live_server_url, E2E_DISP_RECIPE_1)["name"] before_ings = recipe_ingredient_snapshot(page, live_server_url, E2E_DISP_RECIPE_1) assert len(before_ings) == 1 assert before_ings[0][0] == E2E_DISP_ING_1 page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Не должно сохраниться") page.locator('[data-action="add-ingredient"]').click() page.wait_for_function( "() => document.querySelectorAll('#ingredientsTableBody .ingredient-row').length > 1" ) page.locator("#ingredientsTableBody .weight-per-head-input").first.fill("99.9") page.locator('[data-action="show-recipe-edit-off"]').first.click() wait_editor_closed(page) assert fetch_recipe(page, live_server_url, E2E_DISP_RECIPE_1)["name"] == before_name assert recipe_ingredient_snapshot(page, live_server_url, E2E_DISP_RECIPE_1) == before_ings page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_2}"]').click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Второй переименован") with page.expect_response( lambda r: r.request.method == "PUT" and E2E_DISP_RECIPE_2 in r.url, timeout=15000, ): page.locator('[data-action="save-and-exit"]').click() wait_editor_closed(page) assert fetch_recipe(page, live_server_url, E2E_DISP_RECIPE_1)["name"] == before_name assert recipe_ingredient_snapshot(page, live_server_url, E2E_DISP_RECIPE_1) == before_ings assert fetch_recipe(page, live_server_url, E2E_DISP_RECIPE_2)["name"] == "E2E Второй переименован" def test_journey_move_buttons_yoyo(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_MOVE_BUTTONS_YOYO # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) click_move_recipe(page, E2E_DISP_RECIPE_1, "down") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] ) click_move_recipe(page, E2E_DISP_RECIPE_1, "up") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) click_move_recipe(page, E2E_DISP_RECIPE_1, "down") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] ) def test_journey_double_copy_paste_same_buffer(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_DOUBLE_COPY_PASTE # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) click_copy_recipe(page, E2E_DISP_RECIPE_1) select_period(page, E2E_PERIOD_B, live_server_url=live_server_url, expected_order=[]) pasted_ids: list[str] = [] for n in range(2): page.locator(f'[data-action="paste-recipe"][data-period-id="{E2E_PERIOD_B}"]').click() wait_editor_open(page) page.locator("#recipeName").fill(f"E2E Двойная копия {n}") with page.expect_response( lambda r: r.request.method == "POST" and f"/api/periods/{E2E_PERIOD_B}/recipes" in r.url, timeout=20000, ) as resp: page.locator('[data-action="save-and-exit"]').click() rid = resp.value.json().get("id") assert rid pasted_ids.append(rid) wait_editor_closed(page) wait_period_order(page, live_server_url, E2E_PERIOD_B, pasted_ids.copy()) assert_period_order(page, live_server_url, E2E_PERIOD_B, pasted_ids) for rid in pasted_ids: select_period(page, E2E_PERIOD_B, wait_recipe_id=rid) remaining = [x for x in pasted_ids if x != rid] delete_recipe_unlink( page, rid, live_server_url=live_server_url, period_id=E2E_PERIOD_B, expected_period_order=remaining, ) assert_period_order(page, live_server_url, E2E_PERIOD_B, []) def test_journey_mill_dispenser_pingpong(authenticated_page, live_server_url) -> None: SCENARIO_JOURNEY_MILL_DISPENSER_PINGPONG # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) mill_before = fetch_recipe(page, live_server_url, E2E_RECIPE_ID)["name"] page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_RECIPE_ID}"]').click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Mill черновик") page.locator('[data-action="show-recipe-edit-off"]').first.click() wait_editor_closed(page) assert fetch_recipe(page, live_server_url, E2E_RECIPE_ID)["name"] == mill_before _open_dispenser_period_a(page, live_server_url) disp_before = recipe_ingredient_snapshot(page, live_server_url, E2E_DISP_RECIPE_1) page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click() wait_editor_open(page) page.locator("#toggleDryMatterMode").click() page.locator('[data-action="show-recipe-edit-off"]').first.click() wait_editor_closed(page) assert recipe_ingredient_snapshot(page, live_server_url, E2E_DISP_RECIPE_1) == disp_before page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_RECIPE_ID}"]').click() wait_editor_open(page) page.locator("#recipeName").fill("E2E Mill сохранён") with page.expect_response( lambda r: r.request.method == "PUT" and E2E_RECIPE_ID in r.url, timeout=15000, ): page.locator('[data-action="save-and-exit"]').click() wait_editor_closed(page) assert fetch_recipe(page, live_server_url, E2E_RECIPE_ID)["name"] == "E2E Mill сохранён" _open_dispenser_period_a(page, live_server_url) drag_transfer_to_period( page, E2E_DISP_RECIPE_1, E2E_PERIOD_B, live_server_url=live_server_url, source_period_id=E2E_PERIOD_A, expected_source_order=[E2E_DISP_RECIPE_2], expected_target_order=[E2E_DISP_RECIPE_1], ) def test_journey_distracted_help_settings_then_transfer( authenticated_page, live_server_url ) -> None: SCENARIO_JOURNEY_DISTRACTED_UI # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) _open_dispenser_period_a(page, live_server_url) page.locator(f'[data-action="select-recipe"][data-recipe-id="{E2E_DISP_RECIPE_1}"]').click() wait_editor_open(page) page.set_viewport_size(MOBILE_VIEWPORT) page.locator('[data-action="open-recipe-help"]').first.click() page.wait_for_selector("body.recipe-help-modal-open", timeout=5000) page.locator('[data-action="close-recipe-help"]').first.click() page.wait_for_function("() => !document.body.classList.contains('recipe-help-modal-open')") page.set_viewport_size({"width": 1280, "height": 800}) page.locator('[data-action="open-settings"]').first.click(force=True) page.locator('[data-action="toggle-sync-form"]').click() page.locator('[data-action="toggle-credentials-form"]').click() page.locator('#settingsModal [data-action="close-settings"]').first.click() page.wait_for_selector("#settingsModal", state="hidden", timeout=5000) page.locator('[data-action="show-recipe-edit-off"]').first.click() wait_editor_closed(page) restore_two_recipe_order_if_reversed( page, live_server_url, E2E_PERIOD_A, E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2 ) assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) abort_transfer_via_escape(page, E2E_DISP_RECIPE_2) restore_two_recipe_order_if_reversed( page, live_server_url, E2E_PERIOD_A, E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2 ) assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) drag_transfer_to_period( page, E2E_DISP_RECIPE_2, E2E_PERIOD_B, live_server_url=live_server_url, source_period_id=E2E_PERIOD_A, expected_source_order=[E2E_DISP_RECIPE_1], expected_target_order=[E2E_DISP_RECIPE_2], ) def test_journey_mega_mix_all_operations(authenticated_page, live_server_url) -> None: """Мега-сценарий: всё подряд, как реальный зоотехник в понедельник утром.""" SCENARIO_JOURNEY_MEGA_MIX # noqa: F841 page = authenticated_page page.set_viewport_size({"width": 1280, "height": 800}) page.locator('[data-action="open-settings"]').first.click(force=True) page.locator('#settingsModal [data-action="close-settings"]').first.click() page.wait_for_selector("#settingsModal", state="hidden", timeout=5000) _open_dispenser_period_a(page, live_server_url) select_period(page, E2E_PERIOD_B, live_server_url=live_server_url, expected_order=[]) select_period( page, E2E_PERIOD_A, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2], ) click_move_recipe(page, E2E_DISP_RECIPE_1, "down") wait_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] ) abort_transfer_via_escape(page, E2E_DISP_RECIPE_1) ensure_two_recipe_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1], ) assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_2, E2E_DISP_RECIPE_1] ) ensure_two_recipe_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2], ) assert_period_order( page, live_server_url, E2E_PERIOD_A, [E2E_DISP_RECIPE_1, E2E_DISP_RECIPE_2] ) click_copy_recipe(page, E2E_DISP_RECIPE_2) drag_transfer_to_period( page, E2E_DISP_RECIPE_1, E2E_PERIOD_B, live_server_url=live_server_url, source_period_id=E2E_PERIOD_A, expected_source_order=[E2E_DISP_RECIPE_2], expected_target_order=[E2E_DISP_RECIPE_1], ) select_period( page, E2E_PERIOD_B, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1], ) page.locator(f'[data-action="paste-recipe"][data-period-id="{E2E_PERIOD_B}"]').click() wait_editor_open(page) with page.expect_response( lambda r: r.request.method == "POST" and f"/api/periods/{E2E_PERIOD_B}/recipes" in r.url, timeout=20000, ) as paste_resp: page.locator('[data-action="save-and-exit"]').click() pasted_id = paste_resp.value.json().get("id") assert pasted_id wait_editor_closed(page) wait_period_order( page, live_server_url, E2E_PERIOD_B, [E2E_DISP_RECIPE_1, pasted_id] ) open_dispenser_period( page, E2E_PERIOD_B, wait_recipe_id=E2E_DISP_RECIPE_1, live_server_url=live_server_url, expected_order=[E2E_DISP_RECIPE_1, pasted_id], ) click_move_recipe(page, pasted_id, "up") wait_period_order( page, live_server_url, E2E_PERIOD_B, [pasted_id, E2E_DISP_RECIPE_1] ) page.locator(f'[data-action="select-dispenser"][data-dispenser-id="{E2E_MILL_ID}"]').click() page.wait_for_selector("#createRecipeBtn", timeout=10000) assert fetch_recipe(page, live_server_url, E2E_RECIPE_ID).get("name") open_dispenser_period( page, E2E_PERIOD_B, wait_recipe_id=pasted_id, live_server_url=live_server_url, expected_order=[pasted_id, E2E_DISP_RECIPE_1], ) delete_recipe_unlink( page, pasted_id, live_server_url=live_server_url, period_id=E2E_PERIOD_B, expected_period_order=[E2E_DISP_RECIPE_1], ) assert page.request.get(f"{live_server_url}/api/recipes/{pasted_id}").ok