@@ -1,6 +1,7 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -14,6 +15,9 @@ from devplacepy.services.containers.workspace import flags, naming
|
||||
from devplacepy.utils import make_combined_slug
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
EDITOR_CONTAINER_PORT = 8443
|
||||
TRANSITION_TIMEOUT_MS = 8000
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _workspaces_on():
|
||||
@@ -62,7 +66,20 @@ def _project_for(owner_uid: str, title: str = "WS Project") -> dict:
|
||||
return row
|
||||
|
||||
|
||||
def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
|
||||
@pytest.fixture(scope="module")
|
||||
def editor_listener():
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.listen(16)
|
||||
try:
|
||||
yield sock.getsockname()[1]
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def _workspace_for(
|
||||
project: dict, owner_uid: str, editor_port: int = 0, **overrides
|
||||
) -> dict:
|
||||
payload = {
|
||||
"project_uid": project["uid"],
|
||||
"name": "ws-e2e",
|
||||
@@ -73,10 +90,20 @@ def _workspace_for(project: dict, owner_uid: str, **overrides) -> dict:
|
||||
"tunnel_name": naming.generate(),
|
||||
"ports_json": '[{"host": 20777, "container": 8080, "proto": "tcp"}]',
|
||||
}
|
||||
if editor_port:
|
||||
payload["editor_port"] = EDITOR_CONTAINER_PORT
|
||||
payload["ports_json"] = (
|
||||
f'[{{"host": {editor_port}, "container": {EDITOR_CONTAINER_PORT}, '
|
||||
'"proto": "tcp"}]'
|
||||
)
|
||||
payload.update(overrides)
|
||||
return store.create_instance(payload)
|
||||
|
||||
|
||||
def _phase_view(page, phase: str):
|
||||
return page.locator(f".workspace-phase-view[data-phase-view~='{phase}']")
|
||||
|
||||
|
||||
def test_project_page_offers_the_workspace_entry_point_to_the_owner(alice):
|
||||
page, user = alice
|
||||
project = _project_for(_row_for(user)["uid"], "WS Entry")
|
||||
@@ -104,11 +131,13 @@ def test_project_page_hides_the_workspace_entry_point_from_a_non_owner(bob):
|
||||
assert not page.locator(".context-menu-item:has-text('Workspace')").count()
|
||||
|
||||
|
||||
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(alice):
|
||||
def test_project_page_shows_a_direct_vscode_button_for_a_running_workspace(
|
||||
alice, editor_listener
|
||||
):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Direct")
|
||||
instance = _workspace_for(project, row["uid"])
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
@@ -383,11 +412,11 @@ def test_resetting_editor_preferences_restores_the_site_default(alice):
|
||||
get_table("workspace_editor_prefs").delete(owner_id=row["uid"])
|
||||
|
||||
|
||||
def test_the_editor_launch_control_carries_the_window_profile(alice):
|
||||
def test_the_editor_launch_control_carries_the_window_profile(alice, editor_listener):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Editor Launch")
|
||||
instance = _workspace_for(project, row["uid"])
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
@@ -415,3 +444,88 @@ def test_the_workspace_help_states_that_every_folder_is_trusted(alice):
|
||||
help_card.wait_for(state="visible")
|
||||
expect(help_card).to_contain_text("Restricted Mode")
|
||||
expect(help_card).to_contain_text("dpc")
|
||||
|
||||
|
||||
def test_direct_vscode_button_is_absent_while_the_editor_is_unreachable(alice):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Booting")
|
||||
_workspace_for(project, row["uid"])
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
page.locator(".project-detail-actions").wait_for(state="visible")
|
||||
assert not page.locator(".project-detail-actions a[data-editor-open]").count()
|
||||
|
||||
|
||||
def test_a_running_workspace_whose_editor_is_not_listening_reads_starting(alice):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Not Listening")
|
||||
_workspace_for(project, row["uid"])
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
|
||||
expect(_phase_view(page, "starting")).to_be_visible()
|
||||
expect(_phase_view(page, "starting").locator(".btn.is-loading")).to_be_disabled()
|
||||
expect(_phase_view(page, "ready")).to_be_hidden()
|
||||
assert not page.locator(".workspace-actions a[data-editor-open]").is_visible()
|
||||
|
||||
|
||||
def test_start_shows_progress_and_flips_to_open_editor_when_the_editor_answers(
|
||||
alice, editor_listener
|
||||
):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Start Flow")
|
||||
instance = _workspace_for(
|
||||
project,
|
||||
row["uid"],
|
||||
editor_port=editor_listener,
|
||||
status="stopped",
|
||||
desired_state="stopped",
|
||||
)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
|
||||
start = _phase_view(page, "stopped").locator("button[type='submit']")
|
||||
expect(start).to_be_visible()
|
||||
start.click()
|
||||
expect(_phase_view(page, "starting")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(_phase_view(page, "stopped")).to_be_hidden()
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Starting")
|
||||
assert store.get_instance(instance["uid"])["desired_state"] == "running"
|
||||
|
||||
store.update_instance(instance["uid"], {"status": "running"})
|
||||
launch = page.locator(".workspace-actions a[data-editor-open]")
|
||||
launch.wait_for(state="visible", timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
|
||||
expect(_phase_view(page, "starting")).to_be_hidden()
|
||||
expect(launch).to_have_attribute(
|
||||
"href",
|
||||
f"/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
|
||||
)
|
||||
|
||||
|
||||
def test_stop_shows_progress_until_the_container_is_down(alice, editor_listener):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Stop Flow")
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Ready")
|
||||
_phase_view(page, "ready").locator("form[data-workspace-action] button").click()
|
||||
expect(_phase_view(page, "stopping")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopping")
|
||||
assert store.get_instance(instance["uid"])["desired_state"] == "stopped"
|
||||
|
||||
store.update_instance(instance["uid"], {"status": "stopped"})
|
||||
expect(_phase_view(page, "stopped")).to_be_visible(timeout=TRANSITION_TIMEOUT_MS)
|
||||
expect(page.locator("[data-workspace-phase]")).to_have_text("Stopped")
|
||||
|
||||
Reference in New Issue
Block a user