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:
2026-09-12 20:32:31 +02:00
parent c4f7d01b2d
commit cae139ead9
158 changed files with 6156 additions and 377 deletions
+1 -1
View File
@@ -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")
+28
View File
@@ -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}"
+29
View File
@@ -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
View File
@@ -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())}"
+117
View File
@@ -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"
-19
View File
@@ -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)
-36
View File
@@ -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)
+1 -1
View File
@@ -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"