diff --git a/tests/e2e-playwright/tests/conftest.py b/tests/e2e-playwright/tests/conftest.py index ba978ac25b02..ee86f70953cb 100644 --- a/tests/e2e-playwright/tests/conftest.py +++ b/tests/e2e-playwright/tests/conftest.py @@ -492,9 +492,6 @@ def _( # noqa: C901 template_id: str | None, service_version: str | None, ) -> dict[str, Any]: - assert ( - len(created_project_uuids) == 0 - ), "misuse of this fixture! only 1 study can be opened at a time. Otherwise please modify the fixture" with log_context( logging.INFO, f"Open project in {product_url=} as {is_product_billable=}" ) as ctx: @@ -597,22 +594,29 @@ def wait_for_done(response): yield _ # go back to dashboard and wait for project to close - with ExitStack() as stack: - for project_uuid in created_project_uuids: - ctx = stack.enter_context( - log_context(logging.INFO, f"Wait for closed project {project_uuid=}") - ) - stack.enter_context( - log_in_and_out.expect_event( - "framereceived", - SocketIOProjectClosedWaiter(ctx.logger), - timeout=_PROJECT_CLOSING_TIMEOUT, - ) + with log_context(logging.INFO, "Go back to dashboard") as ctx1: + if page.get_by_test_id("dashboardBtn").is_visible(): + with ExitStack() as stack: + for project_uuid in created_project_uuids: + ctx = stack.enter_context( + log_context( + logging.INFO, f"Wait for closed project {project_uuid=}" + ) + ) + stack.enter_context( + log_in_and_out.expect_event( + "framereceived", + SocketIOProjectClosedWaiter(ctx.logger), + timeout=_PROJECT_CLOSING_TIMEOUT, + ) + ) + if created_project_uuids: + page.get_by_test_id("dashboardBtn").click() + page.get_by_test_id("confirmDashboardBtn").click() + else: + ctx1.logger.warning( + "Cannot go back to dashboard, 'dashboard' button is not visible, we are probably already there" ) - if created_project_uuids: - with log_context(logging.INFO, "Go back to dashboard"): - page.get_by_test_id("dashboardBtn").click() - page.get_by_test_id("confirmDashboardBtn").click() for project_uuid in created_project_uuids: with log_context( diff --git a/tests/e2e-playwright/tests/metamodeling/test_response_surface_modeling.py b/tests/e2e-playwright/tests/metamodeling/test_response_surface_modeling.py index 4b2c8e7e1079..f67b8b078dec 100644 --- a/tests/e2e-playwright/tests/metamodeling/test_response_surface_modeling.py +++ b/tests/e2e-playwright/tests/metamodeling/test_response_surface_modeling.py @@ -1,9 +1,20 @@ +# pylint: disable=logging-fstring-interpolation +# pylint:disable=no-value-for-parameter +# pylint:disable=protected-access +# pylint:disable=redefined-outer-name +# pylint:disable=too-many-arguments +# pylint:disable=too-many-statements +# pylint:disable=unused-argument +# pylint:disable=unused-variable + +import json import logging import re -from collections.abc import Callable +from collections.abc import Callable, Iterator from typing import Any, Final -from playwright.sync_api import Page +import pytest +from playwright.sync_api import APIRequestContext, Page from pydantic import AnyUrl from pytest_simcore.helpers.logging_tools import log_context from pytest_simcore.helpers.playwright import ( @@ -14,10 +25,71 @@ ) _WAITING_FOR_SERVICE_TO_START: Final[int] = 5 * MINUTE +_WAITING_FOR_SERVICE_TO_APPEAR: Final[int] = 2 * MINUTE _DEFAULT_RESPONSE_TO_WAIT_FOR: Final[re.Pattern] = re.compile( r"/flask/list_function_job_collections_for_functionid" ) +_STUDY_FUNCTION_NAME: Final[str] = "playwright_test_study_for_rsm" +_FUNCTION_NAME: Final[str] = "playwright_test_function" + + +@pytest.fixture +def create_function_from_project( + api_request_context: APIRequestContext, + is_product_billable: bool, + product_url: AnyUrl, +) -> Iterator[Callable[[Page, str], dict[str, Any]]]: + created_function_uuids: list[str] = [] + + def _create_function_from_project( + page: Page, + project_uuid: str, + ) -> dict[str, Any]: + with log_context( + logging.INFO, + f"Convert {project_uuid=} / {_STUDY_FUNCTION_NAME} to a function", + ) as ctx: + with page.expect_response(re.compile(rf"/projects/{project_uuid}")): + page.get_by_test_id(f"studyBrowserListItem_{project_uuid}").click() + page.wait_for_timeout(2000) + page.get_by_text("create function").first.click() + page.wait_for_timeout(2000) + + with page.expect_response( + lambda response: re.compile(r"/functions").search(response.url) + is not None + and response.request.method == "POST" + ) as create_function_response: + page.get_by_test_id("create_function_page_btn").click() + assert ( + create_function_response.value.ok + ), f"Failed to create function: {create_function_response.value.status}" + function_data = create_function_response.value.json() + + ctx.logger.info( + "Created function: %s", f"{json.dumps(function_data['data'], indent=2)}" + ) + + page.keyboard.press("Escape") + created_function_uuids.append(function_data["data"]["uuid"]) + return function_data["data"] + + yield _create_function_from_project + + # cleanup the functions + for function_uuid in created_function_uuids: + with log_context( + logging.INFO, + f"Delete function with {function_uuid=} in {product_url=} as {is_product_billable=}", + ): + response = api_request_context.delete( + f"{product_url}v0/functions/{function_uuid}" + ) + assert ( + response.status == 204 + ), f"Unexpected error while deleting project: '{response.json()}'" + def test_response_surface_modeling( page: Page, @@ -29,15 +101,88 @@ def test_response_surface_modeling( service_version: str | None, product_url: AnyUrl, is_service_legacy: bool, + create_function_from_project: Callable[[Page, str], dict[str, Any]], ): + # 1. create the initial study with jsonifier + with log_context(logging.INFO, "Create new study for function"): + jsonifier_project_data = create_project_from_service_dashboard( + ServiceType.COMPUTATIONAL, "jsonifier", None, service_version + ) + assert ( + "workbench" in jsonifier_project_data + ), "Expected workbench to be in project data!" + assert isinstance( + jsonifier_project_data["workbench"], dict + ), "Expected workbench to be a dict!" + node_ids: list[str] = list(jsonifier_project_data["workbench"]) + assert len(node_ids) == 1, "Expected 1 node in the workbench!" + + # select the jsonifier, it's the second one as the study has the same name + page.get_by_test_id("nodeTreeItem").filter(has_text="jsonifier").all()[ + 1 + ].click() + + # create the probe + with page.expect_response( + lambda response: re.compile( + rf"/projects/{jsonifier_project_data['uuid']}" + ).search(response.url) + is not None + and response.request.method == "PATCH" + ): + page.get_by_test_id("connect_probe_btn_number_3").click() + + # # create the parameter + page.get_by_test_id("connect_input_btn_number_1").click() + with page.expect_response( + lambda response: re.compile( + rf"/projects/{jsonifier_project_data['uuid']}" + ).search(response.url) + is not None + and response.request.method == "PATCH" + ): + page.get_by_text("new parameter").click() + + # rename the project to identify it + page.get_by_test_id("studyTitleRenamer").click() + with page.expect_response( + lambda response: re.compile( + rf"/projects/{jsonifier_project_data['uuid']}" + ).search(response.url) + is not None + and response.request.method == "PATCH" + ): + page.get_by_test_id("studyTitleRenamer").locator("input").fill( + _STUDY_FUNCTION_NAME + ) + + # 2. go back to dashboard with ( - log_context( - logging.INFO, - f"Waiting for {service_key} to be responsive (waiting for {_DEFAULT_RESPONSE_TO_WAIT_FOR})", - ), - page.expect_response( - _DEFAULT_RESPONSE_TO_WAIT_FOR, timeout=_WAITING_FOR_SERVICE_TO_START - ), + log_context(logging.INFO, "Go back to dashboard"), + page.expect_response(re.compile(r"/projects\?.+")) as list_projects_response, + ): + page.get_by_test_id("dashboardBtn").click() + page.get_by_test_id("confirmDashboardBtn").click() + assert ( + list_projects_response.value.ok + ), f"Failed to list projects: {list_projects_response.value.status}" + project_listing = list_projects_response.value.json() + assert "data" in project_listing + assert len(project_listing["data"]) > 0 + # find the project we just created, it's the first one + our_project = project_listing["data"][0] + assert ( + our_project["name"] == _STUDY_FUNCTION_NAME + ), f"Expected to find our project named {_STUDY_FUNCTION_NAME} in {project_listing}" + + # 3. convert it to a function + create_function_from_project(page, our_project["uuid"]) + + # 3. start a RSM with that function + + with log_context( + logging.INFO, + f"Waiting for {service_key} to be responsive (waiting for {_DEFAULT_RESPONSE_TO_WAIT_FOR})", ): project_data = create_project_from_service_dashboard( ServiceType.DYNAMIC, service_key, None, service_version @@ -58,3 +203,26 @@ def test_response_surface_modeling( product_url=product_url, is_service_legacy=is_service_legacy, ) + + service_iframe = page.frame_locator("iframe") + with log_context(logging.INFO, "Waiting for the RSM to be ready..."): + service_iframe.get_by_role("grid").wait_for( + state="visible", timeout=_WAITING_FOR_SERVICE_TO_APPEAR + ) + + page.wait_for_timeout(10000) + + # # select the function + # service_iframe.get_by_role("gridcell", name=_FUNCTION_NAME).click() + + # # Find the first input field (textbox) in the iframe + # min_input_field = service_iframe.get_by_role("textbox").nth(0) + # min_input_field.fill("1") + # max_input_field = service_iframe.get_by_role("textbox").nth(1) + # max_input_field.fill("10") + + # # click on next + # service_iframe.get_by_role("button", name="Next").click() + + # # then we wait a long time + # page.wait_for_timeout(1 * MINUTE)