"""The actor profile page: avatar/bio editing, and the mods grid.""" import re from playwright.sync_api import expect from helpers import Actor PROFILE_COLLECTION = "dev.starhaven.actor.profile" PNG = bytes.fromhex( "89504e470d0a1a0a0000000d494844520000000100000001080600000" "01f15c4890000000a4944415478da6360000002000155009f490c0000" "000049454e44ae426082" ) def test_owner_can_set_an_avatar(actor: Actor): actor.login() actor.page.goto(f"{actor.app_url}/{actor.handle}") actor.page.locator("#avatar-add").set_input_files( files=[{"name": "avatar.png", "mimeType": "image/png", "buffer": PNG}] ) # The src changes from the default avatar to a blob: URL on success. expect(actor.page.locator("#actor-avatar-img")).to_have_attribute( "src", re.compile(r"^blob:"), timeout=10000 ) # The avatar saves immediately — no form submit needed. [profile] = actor.records(PROFILE_COLLECTION) assert profile["value"]["avatar"]["$type"] == "blob" def test_owner_can_set_a_bio(actor: Actor): actor.login() actor.page.goto(f"{actor.app_url}/{actor.handle}") actor.page.locator("#description").fill("Hi, I make mods.") with actor.page.expect_response(lambda r: r.request.method == "PATCH") as resp: actor.page.get_by_role("button", name="Save").click() assert resp.value.ok [profile] = actor.records(PROFILE_COLLECTION) assert profile["value"]["description"] == "Hi, I make mods." def set_display_name(actor: Actor, name: str) -> None: actor.page.goto(f"{actor.app_url}/{actor.handle}") actor.page.locator("#display-name").fill(name) with actor.page.expect_navigation(): with actor.page.expect_response(lambda r: r.request.method == "PATCH") as resp: actor.page.get_by_role("button", name="Save").click() assert resp.value.ok def test_owner_can_set_a_display_name_and_the_handle_stays_in_view(actor: Actor): actor.login() set_display_name(actor, " Wry mouth\u202e ") [profile] = actor.records(PROFILE_COLLECTION) assert profile["value"]["displayName"] == "Wry mouth" expect(actor.page.get_by_role("heading", level=1)).to_have_text("Wry mouth") expect(actor.page.locator(".actor-handle")).to_have_text(f"@{actor.handle}") def test_a_display_name_is_shown_instead_of_the_handle_on_cards_and_mod_pages( actor: Actor, ): actor.login() actor.create_mod("master-quest", title="Master Quest") set_display_name(actor, "Wrymouth") actor.page.goto(f"{actor.app_url}/{actor.handle}/master-quest") credit = actor.page.locator(".mod-credit-name") expect(credit).to_have_text("Wrymouth") expect(credit).to_have_attribute("href", f"/{actor.handle}") expect(actor.page.locator(".mod-credit-handle")).to_have_text(f"@{actor.handle}") actor.page.goto(f"{actor.app_url}/{actor.handle}") expect(actor.page.locator(".mod-card-byline .byline-names")).to_have_text("Wrymouth") def test_a_user_can_be_found_by_display_name(actor: Actor): actor.login() set_display_name(actor, "Wrymouth") actor.page.goto(f"{actor.app_url}/search?q=wrymouth&type=users") expect(actor.page.locator(".search-users")).to_contain_text("Wrymouth") expect(actor.page.locator(".search-users")).to_contain_text(f"@{actor.handle}") def test_a_display_name_that_is_too_long_is_refused(actor: Actor): actor.login() actor.page.goto(f"{actor.app_url}/{actor.handle}") # The field's own limit would stop this being typed. actor.page.locator("#display-name").evaluate("el => el.removeAttribute('maxlength')") actor.page.locator("#display-name").fill("a" * 65) with actor.page.expect_response(lambda r: r.request.method == "PATCH") as resp: actor.page.get_by_role("button", name="Save").click() assert resp.value.status == 400 assert actor.records(PROFILE_COLLECTION) == [] def test_avatar_persists_after_bio_save(actor: Actor): """Setting a bio must not erase a previously-uploaded avatar.""" actor.login() actor.page.goto(f"{actor.app_url}/{actor.handle}") actor.page.locator("#avatar-add").set_input_files( files=[{"name": "avatar.png", "mimeType": "image/png", "buffer": PNG}] ) expect(actor.page.locator("#actor-avatar-img")).to_have_attribute( "src", re.compile(r"^blob:"), timeout=10000 ) actor.page.locator("#description").fill("Still has avatar") with actor.page.expect_response(lambda r: r.request.method == "PATCH") as resp: actor.page.get_by_role("button", name="Save").click() assert resp.value.ok [profile] = actor.records(PROFILE_COLLECTION) assert profile["value"]["avatar"]["$type"] == "blob" assert profile["value"]["description"] == "Still has avatar" def test_a_visitor_sees_no_edit_controls(actor: Actor): actor.login() actor.page.goto(f"{actor.app_url}/{actor.handle}") actor.page.locator("#description").fill("My bio") # Saving redirects back to the profile, which has to land before the # next navigation or it interrupts it. with actor.page.expect_navigation(): with actor.page.expect_response(lambda r: r.request.method == "PATCH") as resp: actor.page.get_by_role("button", name="Save").click() assert resp.value.ok actor.page.context.clear_cookies() actor.page.goto(f"{actor.app_url}/{actor.handle}") expect(actor.page.get_by_text("My bio")).to_be_visible() expect(actor.page.locator("#avatar-add")).to_have_count(0) expect(actor.page.locator("#profile-form")).to_have_count(0) def test_the_actors_mods_show_as_cards(actor: Actor): actor.login() actor.create_mod("master-quest", title="Master Quest") actor.page.goto(f"{actor.app_url}/{actor.handle}") expect(actor.page.locator("ul.mod-grid")).to_be_visible() expect(actor.page.get_by_role("link", name="Master Quest")).to_be_visible()