diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py index 22ea24baad0..e6c1bf3cabe 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py @@ -10,9 +10,8 @@ from enum import Enum, unique from typing import Any, Final -import httpx from playwright._impl._sync_base import EventContextManager -from playwright.sync_api import FrameLocator, Page, Request +from playwright.sync_api import APIRequestContext, FrameLocator, Page, Request from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from playwright.sync_api import WebSocket from pydantic import AnyUrl @@ -179,7 +178,7 @@ def _attempt_reconnect(self, logger: logging.Logger) -> None: self.ws.expect_event(event, predicate) except Exception as e: # pylint: disable=broad-except - logger.error("🚨 Failed to reconnect WebSocket: %s", e) + logger.exception("🚨 Failed to reconnect WebSocket: %s", e) def expect_event( self, @@ -294,6 +293,7 @@ class SocketIONodeProgressCompleteWaiter: node_id: str logger: logging.Logger product_url: AnyUrl + api_request_context: APIRequestContext _current_progress: dict[NodeProgressType, float] = field( default_factory=defaultdict ) @@ -326,7 +326,7 @@ def __call__(self, message: str) -> bool: self.logger.info( "Current startup progress [expected number of node-progress-types=%d]: %s", len(NodeProgressType.required_types_for_started_service()), - f"{json.dumps({k:round(v,1) for k,v in self._current_progress.items()})}", + f"{json.dumps({k: round(v, 2) for k, v in self._current_progress.items()})}", ) return self.got_expected_node_progress_types() and all( @@ -337,23 +337,25 @@ def __call__(self, message: str) -> bool: _current_timestamp = datetime.now(UTC) if _current_timestamp - self._last_poll_timestamp > timedelta(seconds=5): url = f"https://{self.node_id}.services.{self.get_partial_product_url()}" - response = httpx.get(url, timeout=10) - self.logger.info( - "Querying the service endpoint from the E2E test. Url: %s Response: %s TIP: %s", + response = self.api_request_context.get(url, timeout=1000) + level = logging.DEBUG + if (response.status >= 400) and (response.status not in (502, 503)): + level = logging.ERROR + self.logger.log( + level, + "Querying service endpoint in case we missed some websocket messages. Url: %s Response: '%s' TIP: %s", url, - response, + f"{response.status}: {response.text()}", ( - "Response 401 is OK. It means that service is ready." - if response.status_code == 401 - else "We are emulating the frontend; a 500 response is acceptable if the service is not yet ready." + "We are emulating the frontend; a 5XX response is acceptable if the service is not yet ready." ), ) - if response.status_code <= 401: + + if response.status <= 400: # NOTE: If the response status is less than 400, it means that the backend is ready (There are some services that respond with a 3XX) - # MD: for now I have included 401 - as this also means that backend is ready if self.got_expected_node_progress_types(): self.logger.warning( - "⚠️ Progress bar didn't receive 100 percent but service is already running: %s ⚠️", # https://github.com/ITISFoundation/osparc-simcore/issues/6449 + "⚠️ Progress bar didn't receive 100 percent but service is already running: %s. TIP: we missed some websocket messages! ⚠️", # https://github.com/ITISFoundation/osparc-simcore/issues/6449 self.get_current_progress(), ) return True @@ -408,8 +410,9 @@ def _node_started_predicate(request: Request) -> bool: def _trigger_service_start(page: Page, node_id: str) -> None: - with log_context(logging.INFO, msg="trigger start button"), page.expect_request( - _node_started_predicate, timeout=35 * SECOND + with ( + log_context(logging.INFO, msg="trigger start button"), + page.expect_request(_node_started_predicate, timeout=35 * SECOND), ): page.get_by_test_id(f"Start_{node_id}").click() @@ -433,12 +436,14 @@ def expected_service_running( logging.INFO, msg=f"Waiting for node to run. Timeout: {timeout}" ) as ctx: waiter = SocketIONodeProgressCompleteWaiter( - node_id=node_id, logger=ctx.logger, product_url=product_url + node_id=node_id, + logger=ctx.logger, + product_url=product_url, + api_request_context=page.request, ) service_running = ServiceRunning(iframe_locator=None) try: - with websocket.expect_event("framereceived", waiter, timeout=timeout): if press_start_button: _trigger_service_start(page, node_id) @@ -475,7 +480,10 @@ def wait_for_service_running( logging.INFO, msg=f"Waiting for node to run. Timeout: {timeout}" ) as ctx: waiter = SocketIONodeProgressCompleteWaiter( - node_id=node_id, logger=ctx.logger, product_url=product_url + node_id=node_id, + logger=ctx.logger, + product_url=product_url, + api_request_context=page.request, ) with websocket.expect_event("framereceived", waiter, timeout=timeout): if press_start_button: diff --git a/tests/e2e-playwright/requirements/_test.txt b/tests/e2e-playwright/requirements/_test.txt index 2c499672c85..a20b172a3be 100644 --- a/tests/e2e-playwright/requirements/_test.txt +++ b/tests/e2e-playwright/requirements/_test.txt @@ -1,31 +1,31 @@ annotated-types==0.7.0 # via pydantic -anyio==4.6.2.post1 +anyio==4.8.0 # via httpx arrow==1.3.0 # via -r requirements/_test.in -certifi==2024.8.30 +certifi==2024.12.14 # via # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.1 # via requests -dnspython==2.6.1 +dnspython==2.7.0 # via email-validator docker==7.1.0 # via -r requirements/_test.in email-validator==2.2.0 # via pydantic -faker==29.0.0 +faker==33.3.1 # via -r requirements/_test.in -greenlet==3.0.3 +greenlet==3.1.1 # via playwright h11==0.14.0 # via httpcore httpcore==1.0.7 # via httpx -httpx==0.27.2 +httpx==0.28.1 # via -r requirements/_test.in idna==3.10 # via @@ -35,25 +35,25 @@ idna==3.10 # requests iniconfig==2.0.0 # via pytest -jinja2==3.1.4 +jinja2==3.1.5 # via pytest-html -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -packaging==24.1 +packaging==24.2 # via # pytest # pytest-sugar -playwright==1.47.0 +playwright==1.49.1 # via pytest-playwright pluggy==1.5.0 # via pytest -pydantic==2.10.3 +pydantic==2.10.5 # via -r requirements/_test.in -pydantic-core==2.27.1 +pydantic-core==2.27.2 # via pydantic pyee==12.0.0 # via playwright -pytest==8.3.3 +pytest==8.3.4 # via # pytest-base-url # pytest-html @@ -69,7 +69,7 @@ pytest-instafail==0.5.0 # via -r requirements/_test.in pytest-metadata==3.1.1 # via pytest-html -pytest-playwright==0.5.2 +pytest-playwright==0.6.2 # via -r requirements/_test.in pytest-runner==6.0.1 # via -r requirements/_test.in @@ -87,26 +87,26 @@ requests==2.32.3 # via # docker # pytest-base-url -six==1.16.0 +six==1.17.0 # via python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx + # via anyio tenacity==9.0.0 # via -r requirements/_test.in -termcolor==2.4.0 +termcolor==2.5.0 # via pytest-sugar text-unidecode==1.3 # via python-slugify -types-python-dateutil==2.9.0.20240906 +types-python-dateutil==2.9.0.20241206 # via arrow typing-extensions==4.12.2 # via + # anyio + # faker # pydantic # pydantic-core # pyee -urllib3==2.2.3 +urllib3==2.3.0 # via # docker # requests diff --git a/tests/e2e-playwright/requirements/_tools.txt b/tests/e2e-playwright/requirements/_tools.txt index bce03abbd9d..014eb93e2fc 100644 --- a/tests/e2e-playwright/requirements/_tools.txt +++ b/tests/e2e-playwright/requirements/_tools.txt @@ -1,24 +1,24 @@ -astroid==3.3.4 +astroid==3.3.8 # via pylint -black==24.8.0 +black==24.10.0 # via -r requirements/../../../requirements/devenv.txt -build==1.2.2 +build==1.2.2.post1 # via pip-tools bump2version==1.0.1 # via -r requirements/../../../requirements/devenv.txt cfgv==3.4.0 # via pre-commit -click==8.1.7 +click==8.1.8 # via # black # pip-tools -dill==0.3.8 +dill==0.3.9 # via pylint -distlib==0.3.8 +distlib==0.3.9 # via virtualenv filelock==3.16.1 # via virtualenv -identify==2.6.1 +identify==2.6.5 # via pre-commit isort==5.13.2 # via @@ -26,7 +26,7 @@ isort==5.13.2 # pylint mccabe==0.7.0 # via pylint -mypy==1.12.0 +mypy==1.14.1 # via -r requirements/../../../requirements/devenv.txt mypy-extensions==1.0.0 # via @@ -34,14 +34,14 @@ mypy-extensions==1.0.0 # mypy nodeenv==1.9.1 # via pre-commit -packaging==24.1 +packaging==24.2 # via # -c requirements/_test.txt # black # build pathspec==0.12.1 # via black -pip==24.2 +pip==24.3.1 # via pip-tools pip-tools==7.4.1 # via -r requirements/../../../requirements/devenv.txt @@ -50,11 +50,11 @@ platformdirs==4.3.6 # black # pylint # virtualenv -pre-commit==3.8.0 +pre-commit==4.0.1 # via -r requirements/../../../requirements/devenv.txt -pylint==3.3.0 +pylint==3.3.3 # via -r requirements/../../../requirements/devenv.txt -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools @@ -63,9 +63,9 @@ pyyaml==6.0.2 # -c requirements/../../../requirements/constraints.txt # -c requirements/_test.txt # pre-commit -ruff==0.6.7 +ruff==0.9.1 # via -r requirements/../../../requirements/devenv.txt -setuptools==75.1.0 +setuptools==75.8.0 # via pip-tools tomlkit==0.13.2 # via pylint @@ -73,7 +73,7 @@ typing-extensions==4.12.2 # via # -c requirements/_test.txt # mypy -virtualenv==20.26.5 +virtualenv==20.29.0 # via pre-commit -wheel==0.44.0 +wheel==0.45.1 # via pip-tools diff --git a/tests/e2e-playwright/tests/conftest.py b/tests/e2e-playwright/tests/conftest.py index 43918d8601c..600a314c716 100644 --- a/tests/e2e-playwright/tests/conftest.py +++ b/tests/e2e-playwright/tests/conftest.py @@ -19,7 +19,7 @@ import arrow import pytest from faker import Faker -from playwright.sync_api import APIRequestContext, BrowserContext, Page, expect +from playwright.sync_api import APIRequestContext, Browser, BrowserContext, Page, expect from playwright.sync_api._generated import Playwright from pydantic import AnyUrl, TypeAdapter from pytest_simcore.helpers.faker_factories import DEFAULT_TEST_PASSWORD @@ -329,6 +329,7 @@ def store_browser_context() -> bool: @pytest.fixture def log_in_and_out( + browser: Browser, page: Page, product_url: AnyUrl, user_name: str, @@ -340,7 +341,7 @@ def log_in_and_out( ) -> Iterator[RestartableWebSocket]: with log_context( logging.INFO, - f"Open {product_url=} using {user_name=}/{user_password=}/{auto_register=}", + f"Open {product_url=} using {user_name=}/{user_password=}/{auto_register=} with {browser.browser_type.name}:{browser.version}({browser.browser_type.executable_path})", ): response = page.goto(f"{product_url}") assert response diff --git a/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py b/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py index 0494f7a5f2c..911bb5e6b60 100644 --- a/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py +++ b/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py @@ -14,13 +14,14 @@ from typing import Any, Final, Literal from playwright.sync_api import Page, WebSocket -from pydantic import ByteSize +from pydantic import AnyUrl, ByteSize from pytest_simcore.helpers.logging_tools import log_context from pytest_simcore.helpers.playwright import ( MINUTE, SECOND, RestartableWebSocket, ServiceType, + wait_for_service_running, ) _WAITING_FOR_SERVICE_TO_START: Final[int] = ( @@ -46,45 +47,61 @@ class _JLabTerminalWebSocketWaiter: def __call__(self, message: str) -> bool: with log_context(logging.DEBUG, msg=f"handling websocket {message=}"): decoded_message = json.loads(message) - if ( + return bool( self.expected_message_type == decoded_message[0] and self.expected_message_contents in decoded_message[1] - ): - return True - - return False + ) @dataclass class _JLabWaitForTerminalWebSocket: def __call__(self, new_websocket: WebSocket) -> bool: with log_context(logging.DEBUG, msg=f"received {new_websocket=}"): - if "terminals/websocket" in new_websocket.url: - return True - - return False + return "terminals/websocket" in new_websocket.url def test_jupyterlab( page: Page, + log_in_and_out: RestartableWebSocket, create_project_from_service_dashboard: Callable[ [ServiceType, str, str | None], dict[str, Any] ], service_key: str, large_file_size: ByteSize, large_file_block_size: ByteSize, + product_url: AnyUrl, ): # NOTE: this waits for the jupyter to send message, but is not quite enough - with log_context( - logging.INFO, - f"Waiting for {service_key} to be responsive (waiting for {_SERVICE_NAME_EXPECTED_RESPONSE_TO_WAIT_FOR.get(service_key, _DEFAULT_RESPONSE_TO_WAIT_FOR)})", - ), page.expect_response( - _SERVICE_NAME_EXPECTED_RESPONSE_TO_WAIT_FOR.get( - service_key, _DEFAULT_RESPONSE_TO_WAIT_FOR + with ( + log_context( + logging.INFO, + f"Waiting for {service_key} to be responsive (waiting for {_SERVICE_NAME_EXPECTED_RESPONSE_TO_WAIT_FOR.get(service_key, _DEFAULT_RESPONSE_TO_WAIT_FOR)})", + ), + page.expect_response( + _SERVICE_NAME_EXPECTED_RESPONSE_TO_WAIT_FOR.get( + service_key, _DEFAULT_RESPONSE_TO_WAIT_FOR + ), + timeout=_WAITING_FOR_SERVICE_TO_START, ), - timeout=_WAITING_FOR_SERVICE_TO_START, ): - create_project_from_service_dashboard(ServiceType.DYNAMIC, service_key, None) + project_data = create_project_from_service_dashboard( + ServiceType.DYNAMIC, service_key, None + ) + assert "workbench" in project_data, "Expected workbench to be in project data!" + assert isinstance( + project_data["workbench"], dict + ), "Expected workbench to be a dict!" + node_ids: list[str] = list(project_data["workbench"]) + assert len(node_ids) == 1, "Expected 1 node in the workbench!" + + wait_for_service_running( + page=page, + node_id=node_ids[0], + websocket=log_in_and_out, + timeout=_WAITING_FOR_SERVICE_TO_START, + press_start_button=False, + product_url=product_url, + ) iframe = page.frame_locator("iframe") if service_key == "jupyter-octave-python-math":