11
11
import os
12
12
import random
13
13
import re
14
+ import urllib .parse
14
15
from collections .abc import Callable , Iterator
15
16
from contextlib import ExitStack
16
17
from typing import Any , Final
24
25
from pytest import Item
25
26
from pytest_simcore .helpers .logging_tools import log_context
26
27
from pytest_simcore .helpers .playwright import (
28
+ SECOND ,
27
29
MINUTE ,
28
30
AutoRegisteredUser ,
29
31
RunningState ,
35
37
)
36
38
37
39
_PROJECT_CLOSING_TIMEOUT : Final [int ] = 10 * MINUTE
40
+ _OPENING_NEW_EMPTY_PROJECT_MAX_WAIT_TIME : Final [int ] = 30 * SECOND
41
+ _OPENING_TUTORIAL_MAX_WAIT_TIME : Final [int ] = 3 * MINUTE
38
42
39
43
40
44
def pytest_addoption (parser : pytest .Parser ) -> None :
@@ -94,6 +98,13 @@ def pytest_addoption(parser: pytest.Parser) -> None:
94
98
default = None ,
95
99
help = "Service Key" ,
96
100
)
101
+ group .addoption (
102
+ "--template-id" ,
103
+ action = "store" ,
104
+ type = str ,
105
+ default = None ,
106
+ help = "Template uuid" ,
107
+ )
97
108
group .addoption (
98
109
"--user-agent" ,
99
110
action = "store" ,
@@ -232,6 +243,14 @@ def service_key(request: pytest.FixtureRequest) -> str:
232
243
return os .environ ["SERVICE_KEY" ]
233
244
234
245
246
+ @pytest .fixture (scope = "session" )
247
+ def template_id (request : pytest .FixtureRequest ) -> str | None :
248
+ if key := request .config .getoption ("--template-id" ):
249
+ assert isinstance (key , str )
250
+ return key
251
+ return None
252
+
253
+
235
254
@pytest .fixture (scope = "session" )
236
255
def auto_register (request : pytest .FixtureRequest ) -> bool :
237
256
return bool (request .config .getoption ("--autoregister" ))
@@ -381,6 +400,7 @@ def create_new_project_and_delete(
381
400
def _ (
382
401
expected_states : tuple [RunningState ] = (RunningState .NOT_STARTED ,),
383
402
press_open : bool = True ,
403
+ template_id : str | None = None ,
384
404
) -> dict [str , Any ]:
385
405
assert (
386
406
len (created_project_uuids ) == 0
@@ -390,15 +410,54 @@ def _(
390
410
f"Open project in { product_url = } as { product_billable = } " ,
391
411
) as ctx :
392
412
waiter = SocketIOProjectStateUpdatedWaiter (expected_states = expected_states )
413
+ timeout = _OPENING_TUTORIAL_MAX_WAIT_TIME if template_id is not None else _OPENING_NEW_EMPTY_PROJECT_MAX_WAIT_TIME
393
414
with (
394
- log_in_and_out .expect_event ("framereceived" , waiter ),
415
+ log_in_and_out .expect_event ("framereceived" , waiter , timeout = timeout + 10 * SECOND ),
395
416
page .expect_response (
396
- re .compile (r"/projects/[^:]+:open" )
417
+ re .compile (r"/projects/[^:]+:open" ),
418
+ timeout = timeout + 5 * SECOND
397
419
) as response_info ,
398
420
):
399
421
# Project detail view pop-ups shows
400
422
if press_open :
401
- page .get_by_test_id ("openResource" ).click ()
423
+ open_button = page .get_by_test_id ("openResource" )
424
+ if template_id is not None :
425
+ # it returns a Long Running Task
426
+ with page .expect_response (
427
+ re .compile (rf"/projects\?from_study\={ template_id } " )
428
+ ) as lrt :
429
+ open_button .click ()
430
+ lrt_data = lrt .value .json ()
431
+ lrt_data = lrt_data ["data" ]
432
+ with log_context (
433
+ logging .INFO ,
434
+ "Copying template data" ,
435
+ ) as copying_logger :
436
+ # From the long running tasks response's urls, only their path is relevant
437
+ def url_to_path (url ):
438
+ return urllib .parse .urlparse (url ).path
439
+ def wait_for_done (response ):
440
+ if url_to_path (response .url ) == url_to_path (lrt_data ["status_href" ]):
441
+ resp_data = response .json ()
442
+ resp_data = resp_data ["data" ]
443
+ assert "task_progress" in resp_data
444
+ task_progress = resp_data ["task_progress" ]
445
+ copying_logger .logger .info (
446
+ "task progress: %s %s" ,
447
+ task_progress ["percent" ],
448
+ task_progress ["message" ],
449
+ )
450
+ return False
451
+ if url_to_path (response .url ) == url_to_path (lrt_data ["result_href" ]):
452
+ copying_logger .logger .info ("project created" )
453
+ return response .status == 201
454
+ return False
455
+ with page .expect_response (wait_for_done , timeout = timeout ):
456
+ # if the above calls go to fast, this test could fail
457
+ # not expected in the sim4life context though
458
+ ...
459
+ else :
460
+ open_button .click ()
402
461
if product_billable :
403
462
# Open project with default resources
404
463
page .get_by_test_id ("openWithResources" ).click ()
@@ -466,6 +525,22 @@ def _(plus_button_test_id: str) -> None:
466
525
return _
467
526
468
527
528
+ @pytest .fixture
529
+ def find_and_click_template_in_dashboard (
530
+ page : Page ,
531
+ ) -> Callable [[str ], None ]:
532
+ def _ (template_id : str ) -> None :
533
+ with log_context (logging .INFO , f"Finding { template_id = } in dashboard" ):
534
+ page .get_by_test_id ("templatesTabBtn" ).click ()
535
+ _textbox = page .get_by_test_id ("searchBarFilter-textField-template" )
536
+ _textbox .fill (template_id )
537
+ _textbox .press ("Enter" )
538
+ test_id = "templateBrowserListItem_" + template_id
539
+ page .get_by_test_id (test_id ).click ()
540
+
541
+ return _
542
+
543
+
469
544
@pytest .fixture
470
545
def find_and_start_service_in_dashboard (
471
546
page : Page ,
@@ -478,7 +553,7 @@ def _(
478
553
_textbox = page .get_by_test_id ("searchBarFilter-textField-service" )
479
554
_textbox .fill (service_name )
480
555
_textbox .press ("Enter" )
481
- test_id = f"studyBrowserListItem_simcore /services/{ 'dynamic' if service_type is ServiceType .DYNAMIC else 'comp' } "
556
+ test_id = f"serviceBrowserListItem_simcore /services/{ 'dynamic' if service_type is ServiceType .DYNAMIC else 'comp' } "
482
557
if service_key_prefix :
483
558
test_id = f"{ test_id } /{ service_key_prefix } "
484
559
test_id = f"{ test_id } /{ service_name } "
@@ -502,6 +577,19 @@ def _(plus_button_test_id: str) -> dict[str, Any]:
502
577
return _
503
578
504
579
580
+ @pytest .fixture
581
+ def create_project_from_template_dashboard (
582
+ find_and_click_template_in_dashboard : Callable [[str ], None ],
583
+ create_new_project_and_delete : Callable [[tuple [RunningState ]], dict [str , Any ]],
584
+ ) -> Callable [[ServiceType , str , str | None ], dict [str , Any ]]:
585
+ def _ (template_id : str ) -> dict [str , Any ]:
586
+ find_and_click_template_in_dashboard (template_id )
587
+ expected_states = (RunningState .UNKNOWN ,)
588
+ return create_new_project_and_delete (expected_states , True , template_id )
589
+
590
+ return _
591
+
592
+
505
593
@pytest .fixture
506
594
def create_project_from_service_dashboard (
507
595
find_and_start_service_in_dashboard : Callable [[ServiceType , str , str | None ], None ],
@@ -516,7 +604,7 @@ def _(
516
604
expected_states = (RunningState .UNKNOWN ,)
517
605
if service_type is ServiceType .COMPUTATIONAL :
518
606
expected_states = (RunningState .NOT_STARTED ,)
519
- return create_new_project_and_delete (expected_states )
607
+ return create_new_project_and_delete (expected_states , True )
520
608
521
609
return _
522
610
0 commit comments