Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎨 [e2e] Start from template playwright test #6225

Merged
merged 33 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bb69444
create_project_from_template_dashboard
odeimaiz Aug 23, 2024
c8c085d
test_template added
odeimaiz Aug 23, 2024
32e8831
template_id
odeimaiz Aug 23, 2024
6f51087
fix card id
odeimaiz Aug 23, 2024
c27b3e5
LongRunningTaskWaiter
odeimaiz Aug 23, 2024
043ff8e
wait on Waiter
odeimaiz Aug 23, 2024
881be49
minor
odeimaiz Aug 23, 2024
4908023
fix regex
odeimaiz Aug 26, 2024
a37baab
hardcode template_id
odeimaiz Aug 26, 2024
028a71f
create_template in predicate
odeimaiz Aug 27, 2024
d054bef
working again
odeimaiz Aug 27, 2024
0cfbe6c
relax timeouts
odeimaiz Aug 27, 2024
4ca5803
working and cleaned up
odeimaiz Aug 27, 2024
cc7da70
comment
odeimaiz Aug 27, 2024
7444499
template_id as cli argument
odeimaiz Aug 27, 2024
f7b4409
use common code
sanderegg Aug 27, 2024
9f59a26
More refactoring
odeimaiz Aug 27, 2024
62fe3e1
more refactoring
odeimaiz Aug 27, 2024
c1735a3
minor
odeimaiz Aug 27, 2024
befeb97
with context
odeimaiz Aug 27, 2024
1a1816c
Merge branch 'master' into e2e/wpt-test
odeimaiz Aug 27, 2024
00a1885
minor
odeimaiz Aug 27, 2024
a87af7d
Merge branch 'e2e/wpt-test' of github.com:odeimaiz/osparc-simcore int…
odeimaiz Aug 27, 2024
57f79a8
make linter happy
odeimaiz Aug 27, 2024
9d2177d
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
d8bb4db
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
eb3dd63
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
c45dea5
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
4ccb3b8
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
3678470
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
1849743
Update tests/e2e-playwright/tests/conftest.py
odeimaiz Aug 27, 2024
635193f
consts
odeimaiz Aug 27, 2024
191b79f
minor
odeimaiz Aug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import datetime
import logging
import re
from dataclasses import dataclass
from typing import Dict, Final, Union

import arrow
from playwright.sync_api import FrameLocator, Page, WebSocket, expect

from .logging_tools import log_context
from .playwright import (
SECOND,
MINUTE,
SOCKETIO_MESSAGE_PREFIX,
SocketIOEvent,
decode_socketio_42_message,
wait_for_service_running,
)

_S4L_STREAMING_ESTABLISHMENT_MAX_TIME: Final[int] = 15 * SECOND
_S4L_SOCKETIO_REGEX: Final[re.Pattern] = re.compile(
r"^(?P<protocol>[^:]+)://(?P<node_id>[^\.]+)\.services\.(?P<hostname>[^\/]+)\/socket\.io\/.+$"
)
_EC2_STARTUP_MAX_WAIT_TIME: Final[int] = 1 * MINUTE
_S4L_MAX_STARTUP_TIME: Final[int] = 1 * MINUTE
_S4L_DOCKER_PULLING_MAX_TIME: Final[int] = 10 * MINUTE
_S4L_AUTOSCALED_MAX_STARTUP_TIME: Final[int] = (
_EC2_STARTUP_MAX_WAIT_TIME + _S4L_DOCKER_PULLING_MAX_TIME + _S4L_MAX_STARTUP_TIME
)
_S4L_STARTUP_SCREEN_MAX_TIME: Final[int] = 45 * SECOND


@dataclass(kw_only=True)
class S4LWaitForWebsocket:
logger: logging.Logger

def __call__(self, new_websocket: WebSocket) -> bool:
if re.match(_S4L_SOCKETIO_REGEX, new_websocket.url):
self.logger.info("found S4L websocket!")
return True

return False


@dataclass(kw_only=True)
class _S4LSocketIOCheckBitRateIncreasesMessagePrinter:
observation_time: datetime.timedelta
logger: logging.Logger
_initial_bit_rate: float = 0
_initial_bit_rate_time: datetime.datetime = arrow.utcnow().datetime

def __call__(self, message: str) -> bool:
if message.startswith(SOCKETIO_MESSAGE_PREFIX):
decoded_message: SocketIOEvent = decode_socketio_42_message(message)
if (
decoded_message.name == "server.video_stream.bitrate_data"
and "bitrate" in decoded_message.obj
):
current_bitrate = decoded_message.obj["bitrate"]
if self._initial_bit_rate == 0:
self._initial_bit_rate = current_bitrate
self._initial_bit_rate_time = arrow.utcnow().datetime
self.logger.info(
"%s",
f"{self._initial_bit_rate=} at {self._initial_bit_rate_time.isoformat()}",
)
return False

# NOTE: MaG says the value might also go down, but it shall definitely change,
# if this code proves unsafe we should change it.
elapsed_time = arrow.utcnow().datetime - self._initial_bit_rate_time
if (
elapsed_time > self.observation_time
and "bitrate" in decoded_message.obj
):
current_bitrate = decoded_message.obj["bitrate"]
bitrate_test = bool(self._initial_bit_rate != current_bitrate)
self.logger.info(
"%s",
f"{current_bitrate=} after {elapsed_time=}: {'good!' if bitrate_test else 'failed! bitrate did not change! TIP: talk with MaG about underwater cables!'}",
)
return bitrate_test

return False


def launch_S4L(page: Page, node_id, log_in_and_out: WebSocket, autoscaled: bool) -> Dict[str, Union[WebSocket, FrameLocator]]:
with log_context(logging.INFO, "launch S4L") as ctx:
predicate = S4LWaitForWebsocket(logger=ctx.logger)
with page.expect_websocket(
predicate,
timeout=_S4L_STARTUP_SCREEN_MAX_TIME
+ (
_S4L_AUTOSCALED_MAX_STARTUP_TIME
if autoscaled
else _S4L_MAX_STARTUP_TIME
)
+ 10 * SECOND,
) as ws_info:
s4l_iframe = wait_for_service_running(
page=page,
node_id=node_id,
websocket=log_in_and_out,
timeout=(
_S4L_AUTOSCALED_MAX_STARTUP_TIME
if autoscaled
else _S4L_MAX_STARTUP_TIME
),
press_start_button=False,
)
s4l_websocket = ws_info.value
ctx.logger.info("acquired S4L websocket!")
return {
"websocket": s4l_websocket,
"iframe" : s4l_iframe,
}


def interact_with_S4L(page: Page, s4l_iframe: FrameLocator) -> None:
# Wait until grid is shown
# NOTE: the startup screen should disappear very fast after the websocket was acquired
with log_context(logging.INFO, "Interact with S4l"):
s4l_iframe.get_by_test_id("tree-item-Grid").nth(0).click()
page.wait_for_timeout(3000)


def check_video_streaming(page: Page, s4l_iframe: FrameLocator, s4l_websocket: WebSocket) -> None:
with log_context(logging.INFO, "Check videostreaming works") as ctx:
waiter = _S4LSocketIOCheckBitRateIncreasesMessagePrinter(
observation_time=datetime.timedelta(
milliseconds=_S4L_STREAMING_ESTABLISHMENT_MAX_TIME / 2.0,
),
logger=ctx.logger,
)
with s4l_websocket.expect_event(
"framereceived",
waiter,
timeout=_S4L_STREAMING_ESTABLISHMENT_MAX_TIME,
):
...

expect(
s4l_iframe.locator("video"),
"videostreaming is not established. "
"TIP: if using playwright integrated open source chromIUM, "
"webkit or firefox this is expected, switch to chrome/msedge!!",
).to_be_visible()
s4l_iframe.locator("video").click()
page.wait_for_timeout(3000)
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,8 @@ qx.Class.define("osparc.dashboard.CardBase", {
},

__applyUuid: function(value, old) {
osparc.utils.Utils.setIdToWidget(this, "studyBrowserListItem_"+value);
const resourceType = this.getResourceType() || "study";
osparc.utils.Utils.setIdToWidget(this, resourceType + "BrowserListItem_" + value);

this.setCardKey(value);
},
Expand Down
95 changes: 90 additions & 5 deletions tests/e2e-playwright/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import os
import random
import re
import urllib.parse
from collections.abc import Callable, Iterator
from contextlib import ExitStack
from typing import Any, Final
Expand Down Expand Up @@ -94,6 +95,13 @@ def pytest_addoption(parser: pytest.Parser) -> None:
default=None,
help="Service Key",
)
group.addoption(
"--template-id",
action="store",
type=str,
default=None,
help="Template uuid",
)
group.addoption(
"--user-agent",
action="store",
Expand Down Expand Up @@ -232,6 +240,14 @@ def service_key(request: pytest.FixtureRequest) -> str:
return os.environ["SERVICE_KEY"]


@pytest.fixture(scope="session")
def template_id(request: pytest.FixtureRequest) -> str:
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
if key := request.config.getoption("--template-id"):
assert isinstance(key, str)
return key
return os.environ["TEMPLATE_ID"]
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture(scope="session")
def auto_register(request: pytest.FixtureRequest) -> bool:
return bool(request.config.getoption("--autoregister"))
Expand Down Expand Up @@ -381,6 +397,7 @@ def create_new_project_and_delete(
def _(
expected_states: tuple[RunningState] = (RunningState.NOT_STARTED,),
press_open: bool = True,
template_id: str = None,
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
) -> dict[str, Any]:
assert (
len(created_project_uuids) == 0
Expand All @@ -390,15 +407,54 @@ def _(
f"Open project in {product_url=} as {product_billable=}",
) as ctx:
waiter = SocketIOProjectStateUpdatedWaiter(expected_states=expected_states)
timeout = 60000 if template_id else 180000
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
with (
log_in_and_out.expect_event("framereceived", waiter),
log_in_and_out.expect_event("framereceived", waiter, timeout=timeout),
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
page.expect_response(
re.compile(r"/projects/[^:]+:open")
re.compile(r"/projects/[^:]+:open"),
timeout=timeout
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
) as response_info,
):
# Project detail view pop-ups shows
if press_open:
page.get_by_test_id("openResource").click()
open_button = page.get_by_test_id("openResource")
if template_id:
odeimaiz marked this conversation as resolved.
Show resolved Hide resolved
# it returns a Long Running Task
with page.expect_response(
re.compile(rf"/projects\?from_study\={template_id}")
) as lrt:
open_button.click()
lrt_data = lrt.value.json()
lrt_data = lrt_data["data"]
with log_context(
logging.INFO,
f"Copying template data",
) as copying_logger:
# From the long running tasks response's urls, only their path is relevant
def url_to_path(url):
return urllib.parse.urlparse(url).path
def wait_for_done(response):
if url_to_path(response.url) == url_to_path(lrt_data["status_href"]):
resp_data = response.json()
resp_data = resp_data["data"]
assert "task_progress" in resp_data
task_progress = resp_data["task_progress"]
copying_logger.logger.info(
"task progress: %s %s",
task_progress["percent"],
task_progress["message"],
)
return False
if url_to_path(response.url) == url_to_path(lrt_data["result_href"]):
copying_logger.logger.info("project created")
return response.status == 201
return False
with page.expect_response(wait_for_done, timeout=timeout):
# if the above calls go to fast, this test could fail
# not expected in the sim4life context though
...
else:
open_button.click()
if product_billable:
# Open project with default resources
page.get_by_test_id("openWithResources").click()
Expand Down Expand Up @@ -466,6 +522,22 @@ def _(plus_button_test_id: str) -> None:
return _


@pytest.fixture
def find_and_click_template_in_dashboard(
page: Page,
) -> Callable[[str], None]:
def _(template_id: str) -> None:
with log_context(logging.INFO, f"Finding {template_id=} in dashboard"):
page.get_by_test_id("templatesTabBtn").click()
_textbox = page.get_by_test_id("searchBarFilter-textField-template")
_textbox.fill(template_id)
_textbox.press("Enter")
test_id = "templateBrowserListItem_" + template_id
page.get_by_test_id(test_id).click()

return _


@pytest.fixture
def find_and_start_service_in_dashboard(
page: Page,
Expand All @@ -478,7 +550,7 @@ def _(
_textbox = page.get_by_test_id("searchBarFilter-textField-service")
_textbox.fill(service_name)
_textbox.press("Enter")
test_id = f"studyBrowserListItem_simcore/services/{'dynamic' if service_type is ServiceType.DYNAMIC else 'comp'}"
test_id = f"serviceBrowserListItem_simcore/services/{'dynamic' if service_type is ServiceType.DYNAMIC else 'comp'}"
if service_key_prefix:
test_id = f"{test_id}/{service_key_prefix}"
test_id = f"{test_id}/{service_name}"
Expand All @@ -502,6 +574,19 @@ def _(plus_button_test_id: str) -> dict[str, Any]:
return _


@pytest.fixture
def create_project_from_template_dashboard(
find_and_click_template_in_dashboard: Callable[[str], None],
create_new_project_and_delete: Callable[[tuple[RunningState]], dict[str, Any]],
) -> Callable[[ServiceType, str, str | None], dict[str, Any]]:
def _(template_id: str) -> dict[str, Any]:
find_and_click_template_in_dashboard(template_id)
expected_states = (RunningState.UNKNOWN,)
return create_new_project_and_delete(expected_states, True, template_id)

return _


@pytest.fixture
def create_project_from_service_dashboard(
find_and_start_service_in_dashboard: Callable[[ServiceType, str, str | None], None],
Expand All @@ -516,7 +601,7 @@ def _(
expected_states = (RunningState.UNKNOWN,)
if service_type is ServiceType.COMPUTATIONAL:
expected_states = (RunningState.NOT_STARTED,)
return create_new_project_and_delete(expected_states)
return create_new_project_and_delete(expected_states, True)

return _

Expand Down
Loading
Loading