forked from retoor/devplacepy
Add personal notes, DeepSearch history, backup offload, and gateway auth throttling
Also streamline the top navigation: drop the Tools dropdown, make Quizzes and Battles icon-only entries, and remove the Workspace, Containers and Editor entry points from the project detail page.
This commit is contained in:
@@ -52,7 +52,7 @@ def test_battles_page_lists_cards(alice):
|
||||
_seed_war("Vikings", "Knights")
|
||||
page.goto(f"{BASE_URL}/battles", wait_until="domcontentloaded")
|
||||
page.locator("dp-opinion-war").first.wait_for(state="visible")
|
||||
expect(page.locator(".topnav-link.active:has-text('Battles')")).to_be_visible()
|
||||
expect(page.locator(".topnav-icon.active[href='/battles']")).to_be_visible()
|
||||
expect(page.locator("dp-opinion-war .war-vs").first).to_have_text("VS")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
from tests.e2e.post import create_post
|
||||
|
||||
|
||||
def test_comment_copy_link_button(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "devlog", "Post for comment permalink test")
|
||||
textarea = page.locator(".comment-form textarea[name='content']")
|
||||
textarea.fill("Comment for permalink test")
|
||||
page.locator(".comment-form button:has-text('Post')").click()
|
||||
comment_text = page.locator(".comment-text:has-text('Comment for permalink test')")
|
||||
expect(comment_text).to_be_visible()
|
||||
wrapper = page.locator(".comment-body", has=comment_text)
|
||||
comment_uid = wrapper.get_attribute("data-comment-uid")
|
||||
assert comment_uid
|
||||
copy_btn = page.locator(f".comment-action-btn[data-share='#comment-{comment_uid}']")
|
||||
expect(copy_btn).to_be_visible()
|
||||
copy_btn.click()
|
||||
expect(copy_btn).to_have_text("Copied!", timeout=3000)
|
||||
expect(copy_btn).not_to_have_text("Copied!", timeout=3000)
|
||||
try:
|
||||
clip = page.evaluate("navigator.clipboard.readText()")
|
||||
except Exception:
|
||||
clip = None
|
||||
if clip:
|
||||
assert clip.endswith(f"#comment-{comment_uid}"), f"clipboard={clip!r}"
|
||||
@@ -1018,6 +1018,35 @@ def test_feed_scroll_not_restored_on_fresh_visit(alice):
|
||||
assert page.evaluate("window.scrollY") < 60
|
||||
|
||||
|
||||
def test_feed_right_rail_stays_plain_sticky_when_it_fits(alice):
|
||||
page, user = alice
|
||||
page.set_viewport_size({"width": 1400, "height": 2000})
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
right = page.locator(".feed-right")
|
||||
right.wait_for(state="visible")
|
||||
page.wait_for_timeout(400)
|
||||
assert right.evaluate("el => el.classList.contains('sticky-scroll-active')") is False
|
||||
assert right.evaluate("el => getComputedStyle(el).maxHeight") == "none"
|
||||
|
||||
|
||||
def test_feed_right_rail_scrolls_independently_when_taller_than_viewport(alice):
|
||||
page, user = alice
|
||||
page.set_viewport_size({"width": 1400, "height": 400})
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
right = page.locator(".feed-right")
|
||||
right.wait_for(state="visible")
|
||||
page.wait_for_function(
|
||||
"document.querySelector('.feed-right').classList.contains('sticky-scroll-active')"
|
||||
)
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
right.evaluate("el => el.scrollTo(0, el.scrollHeight)")
|
||||
page.wait_for_timeout(200)
|
||||
right_scroll_top = right.evaluate("el => el.scrollTop")
|
||||
window_scroll_y = page.evaluate("window.scrollY")
|
||||
assert right_scroll_top > 0
|
||||
assert window_scroll_y == 0
|
||||
|
||||
|
||||
def _seed_post_with_long_code_line(topic="devlog"):
|
||||
owner = str(uuid4())
|
||||
get_table("users").insert(
|
||||
|
||||
+40
-2
@@ -230,8 +230,8 @@ def test_delete_gist(alice, app_server):
|
||||
page, _ = alice
|
||||
title = f"Delete Test {int(time.time())}"
|
||||
_create_gist(page, title=title, source_code="delete_me = True")
|
||||
page.locator("button:has-text('Delete')").wait_for(state="visible", timeout=5000)
|
||||
page.click("button:has-text('Delete')")
|
||||
page.locator("button[data-confirm='Delete this gist?']").wait_for(state="visible", timeout=5000)
|
||||
page.click("button[data-confirm='Delete this gist?']")
|
||||
page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
page.wait_for_url(f"{BASE_URL}/gists", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -275,6 +275,44 @@ def test_gist_code_copy_button(alice, app_server):
|
||||
expect(btn).to_have_text("Copied!", timeout=3000)
|
||||
|
||||
|
||||
def test_markdown_gist_raw_rendered_toggle(alice, app_server):
|
||||
from playwright.sync_api import expect
|
||||
|
||||
page, _ = alice
|
||||
_create_gist(
|
||||
page,
|
||||
title=f"Markdown Toggle {int(time.time())}",
|
||||
language="markdown",
|
||||
source_code="# Heading\n\nSome **bold** text.",
|
||||
)
|
||||
toggle = page.locator("button[data-view-toggle='gist-view-raw']")
|
||||
raw_view = page.locator("#gist-view-raw")
|
||||
rendered_view = page.locator("#gist-view-rendered")
|
||||
expect(toggle).to_have_text("View rendered")
|
||||
expect(raw_view).to_be_visible()
|
||||
expect(rendered_view).to_be_hidden()
|
||||
toggle.click()
|
||||
expect(toggle).to_have_text("View raw")
|
||||
expect(rendered_view).to_be_visible()
|
||||
expect(raw_view).to_be_hidden()
|
||||
assert page.locator("#gist-view-rendered h1:has-text('Heading')").count() == 1
|
||||
toggle.click()
|
||||
expect(toggle).to_have_text("View rendered")
|
||||
expect(raw_view).to_be_visible()
|
||||
expect(rendered_view).to_be_hidden()
|
||||
|
||||
|
||||
def test_markdown_rendered_gist_has_no_toggle(alice, app_server):
|
||||
page, _ = alice
|
||||
_create_gist(
|
||||
page,
|
||||
title=f"No Toggle {int(time.time())}",
|
||||
language="markdown_rendered",
|
||||
source_code="# Already rendered",
|
||||
)
|
||||
assert page.locator("button[data-view-toggle]").count() == 0
|
||||
|
||||
|
||||
def test_language_filter(alice, app_server):
|
||||
page, _ = alice
|
||||
title = f"Lang Filter Test {int(time.time())}"
|
||||
|
||||
@@ -406,3 +406,120 @@ def test_live_incoming_message_shows_report_button(alice, bob):
|
||||
bubble = page_b.locator(f".message-bubble.theirs:has-text('{msg}')")
|
||||
bubble.wait_for(state="visible", timeout=10000)
|
||||
expect(bubble.locator(".message-report-btn")).to_be_visible()
|
||||
|
||||
|
||||
def test_send_button_locks_during_send_and_prevents_duplicate(alice, bob):
|
||||
page_a, user_a = alice
|
||||
page_b, user_b = bob
|
||||
uid_b = get_table("users").find_one(username=user_b["username"])["uid"]
|
||||
|
||||
page_a.goto(
|
||||
f"{BASE_URL}/messages?with_uid={uid_b}", wait_until="domcontentloaded"
|
||||
)
|
||||
|
||||
msg = f"Locked send {int(time.time() * 1000)}"
|
||||
textarea = page_a.locator(".messages-input-area textarea[name='content']")
|
||||
textarea.wait_for(state="visible")
|
||||
textarea.fill(msg)
|
||||
|
||||
send_btn = page_a.locator(".messages-send-btn")
|
||||
send_btn.click()
|
||||
|
||||
# the composer refuses a duplicate submit while this send is unconfirmed
|
||||
expect(send_btn).to_be_disabled()
|
||||
|
||||
bubble = page_a.locator(f".message-bubble.mine:has-text('{msg}')")
|
||||
reconciled = page_a.locator(
|
||||
f".message-bubble.mine:has-text('{msg}')[data-msg-uid]:not(.pending)"
|
||||
)
|
||||
reconciled.first.wait_for(state="visible", timeout=10000)
|
||||
|
||||
expect(send_btn).to_be_enabled()
|
||||
assert bubble.count() == 1
|
||||
|
||||
|
||||
def test_scroll_up_is_not_yanked_by_incoming_message(alice, bob):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
page_a, user_a = alice
|
||||
page_b, user_b = bob
|
||||
uid_a = get_table("users").find_one(username=user_a["username"])["uid"]
|
||||
uid_b = get_table("users").find_one(username=user_b["username"])["uid"]
|
||||
|
||||
base = datetime.now(timezone.utc) - timedelta(minutes=5)
|
||||
for i in range(40):
|
||||
get_table("messages").insert(
|
||||
{
|
||||
"uid": generate_uid(),
|
||||
"sender_uid": uid_a if i % 2 == 0 else uid_b,
|
||||
"receiver_uid": uid_b if i % 2 == 0 else uid_a,
|
||||
"content": f"scroll history line {i}",
|
||||
"read": True,
|
||||
"created_at": (base + timedelta(seconds=i)).isoformat(),
|
||||
"updated_at": None,
|
||||
}
|
||||
)
|
||||
|
||||
page_a.goto(
|
||||
f"{BASE_URL}/messages?with_uid={uid_b}", wait_until="domcontentloaded"
|
||||
)
|
||||
page_b.goto(
|
||||
f"{BASE_URL}/messages?with_uid={uid_a}", wait_until="domcontentloaded"
|
||||
)
|
||||
|
||||
thread = page_a.locator(".messages-thread")
|
||||
thread.wait_for(state="visible")
|
||||
thread.locator(".message-bubble").first.wait_for(state="visible")
|
||||
|
||||
thread.evaluate(
|
||||
"el => { el.scrollTop = 0; el.dispatchEvent(new Event('scroll')); }"
|
||||
)
|
||||
page_a.wait_for_timeout(200)
|
||||
scroll_before = thread.evaluate("el => el.scrollTop")
|
||||
assert scroll_before < 50
|
||||
|
||||
msg = f"While scrolled up {int(time.time() * 1000)}"
|
||||
textarea_b = page_b.locator(".messages-input-area textarea[name='content']")
|
||||
textarea_b.wait_for(state="visible")
|
||||
textarea_b.fill(msg)
|
||||
page_b.locator(".messages-send-btn").click()
|
||||
|
||||
page_a.locator(f".message-bubble.theirs:has-text('{msg}')").first.wait_for(
|
||||
state="visible", timeout=10000
|
||||
)
|
||||
scroll_after = thread.evaluate("el => el.scrollTop")
|
||||
assert scroll_after < 50, (
|
||||
"an incoming message must not yank a scrolled-up reader back to the bottom"
|
||||
)
|
||||
|
||||
|
||||
def test_scroll_stays_pinned_to_bottom_for_active_reader(alice, bob):
|
||||
page_a, user_a = alice
|
||||
page_b, user_b = bob
|
||||
uid_a = get_table("users").find_one(username=user_a["username"])["uid"]
|
||||
uid_b = get_table("users").find_one(username=user_b["username"])["uid"]
|
||||
|
||||
page_a.goto(
|
||||
f"{BASE_URL}/messages?with_uid={uid_b}", wait_until="domcontentloaded"
|
||||
)
|
||||
page_b.goto(
|
||||
f"{BASE_URL}/messages?with_uid={uid_a}", wait_until="domcontentloaded"
|
||||
)
|
||||
|
||||
thread = page_a.locator(".messages-thread")
|
||||
thread.wait_for(state="visible")
|
||||
|
||||
msg = f"Pinned bottom {int(time.time() * 1000)}"
|
||||
textarea_b = page_b.locator(".messages-input-area textarea[name='content']")
|
||||
textarea_b.wait_for(state="visible")
|
||||
textarea_b.fill(msg)
|
||||
page_b.locator(".messages-send-btn").click()
|
||||
|
||||
page_a.locator(f".message-bubble.theirs:has-text('{msg}')").first.wait_for(
|
||||
state="visible", timeout=10000
|
||||
)
|
||||
at_bottom = thread.evaluate(
|
||||
"el => el.scrollHeight - el.scrollTop - el.clientHeight < 100"
|
||||
)
|
||||
assert at_bottom, "an active-at-bottom reader must still auto-scroll to new messages"
|
||||
|
||||
@@ -1007,25 +1007,6 @@ def _create_project_containers_menu(key, title, is_private=False):
|
||||
return r.json()["data"]["slug"]
|
||||
|
||||
|
||||
def test_containers_menu_visible_for_admin_on_own_private_project(page, app_server):
|
||||
name, uid, key = _make_admin_containers_menu("cmown")
|
||||
slug = _create_project_containers_menu(key, "Containers Menu Own Private", is_private=True)
|
||||
login_user(page, {"email": f"{name}@t.dev", "password": "secret123"})
|
||||
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
||||
page.locator(".project-actions-more").click()
|
||||
expect(page.locator(".context-menu-item:has-text('Containers')")).to_be_visible()
|
||||
|
||||
|
||||
def test_containers_menu_visible_for_any_admin_on_public_project(page, app_server):
|
||||
_, _, owner_key = _make_admin_containers_menu("cmpubowner")
|
||||
other_name, _, _ = _make_admin_containers_menu("cmpubother")
|
||||
slug = _create_project_containers_menu(owner_key, "Containers Menu Public", is_private=False)
|
||||
login_user(page, {"email": f"{other_name}@t.dev", "password": "secret123"})
|
||||
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
||||
page.locator(".project-actions-more").click()
|
||||
expect(page.locator(".context-menu-item:has-text('Containers')")).to_be_visible()
|
||||
|
||||
|
||||
def test_containers_menu_hidden_for_non_owner_admin_on_member_private_project(page, app_server):
|
||||
_, member_uid, member_key = _signup_containers_menu("cmmember")
|
||||
slug = _create_project_containers_menu(member_key, "Containers Menu Member Private", is_private=True)
|
||||
|
||||
@@ -104,23 +104,6 @@ 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")
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
page.locator(".project-actions-more").click()
|
||||
entry = page.locator(".context-menu-item:has-text('Workspace')")
|
||||
entry.wait_for(state="visible")
|
||||
entry.click()
|
||||
page.wait_for_url(
|
||||
f"{BASE_URL}/projects/{project['slug']}/workspace",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
page.locator(".workspace-page").wait_for(state="visible")
|
||||
|
||||
|
||||
def test_project_page_hides_the_workspace_entry_point_from_a_non_owner(bob):
|
||||
page, user = bob
|
||||
project = _project_for(str(uuid4()), "WS Entry Foreign")
|
||||
@@ -131,25 +114,6 @@ 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, editor_listener
|
||||
):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
project = _project_for(row["uid"], "WS Direct")
|
||||
instance = _workspace_for(project, row["uid"], editor_port=editor_listener)
|
||||
page.goto(
|
||||
f"{BASE_URL}/projects/{project['slug']}", wait_until="domcontentloaded"
|
||||
)
|
||||
button = page.locator(".project-detail-actions a[data-editor-open]")
|
||||
button.wait_for(state="visible")
|
||||
expect(button).to_have_attribute("target", "_blank")
|
||||
expect(button).to_have_attribute(
|
||||
"href",
|
||||
f"/projects/{project['slug']}/containers/instances/{instance['uid']}/code/",
|
||||
)
|
||||
|
||||
|
||||
def test_direct_vscode_button_is_absent_while_the_workspace_is_stopped(alice):
|
||||
page, user = alice
|
||||
row = _row_for(user)
|
||||
|
||||
@@ -10,7 +10,7 @@ LAYOUT = ".feed-layout"
|
||||
LEFT = ".feed-layout > .sidebar-card"
|
||||
CENTRE = ".feed-layout > .feed-main"
|
||||
RIGHT = ".feed-layout > .feed-right"
|
||||
NAV_ENTRY = ".topnav-link[href='/quizzes']"
|
||||
NAV_ENTRY = ".topnav-icon[href='/quizzes']"
|
||||
NEW_QUIZ_BUTTON = ".quiz-actions a[href='/quizzes/new']"
|
||||
DEVII_BUTTON = ".quiz-actions button[data-devii-open]"
|
||||
SCOREBOARD = ".quiz-scoreboard-card"
|
||||
|
||||
Reference in New Issue
Block a user