Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384import { test, expect } from "../../base.js";import { login } from "../../helpers.js";import { MockServer } from "../../mockServer.js";import { createPost, createConvo, createProfile,} from "../../../shared/factories.js";
const postUri = "at://did:plc:author1/app.bsky.feed.post/joinLink1";const postPath = "/profile/author1.bsky.social/post/joinLink1";
function makeJoinLinkPreview(overrides = {}) { return { $type: "chat.bsky.group.defs#joinLinkPreviewView", code: "abcd1234", name: "Friends of Bsky", memberCount: 5, memberLimit: 50, joinRule: "open", requireApproval: false, owner: { did: "did:plc:owner", handle: "owner.bsky.social", displayName: "Owner", avatar: "", viewer: {}, labels: [], createdAt: "2025-01-01T00:00:00.000Z", }, viewer: {}, ...overrides, };}
function buildInvitePost(preview) { return createPost({ uri: postUri, text: "Join us!", authorHandle: "author1.bsky.social", authorDisplayName: "Author One", embed: { $type: "chat.bsky.embed.joinLink#view", joinLinkPreview: preview, }, });}
async function setupInvitePostThread(page, preview, { mockServer } = {}) { const server = mockServer ?? new MockServer(); const post = buildInvitePost(preview); server.addPosts([post]); server.setPostThread(post.uri, { $type: "app.bsky.feed.defs#threadViewPost", post, replies: [], }); await server.setup(page); return server;}
test.describe("Join link embed flows", () => { test.describe("render states", () => { test("renders open action when viewer is a member of a different chat", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ convo: { id: "other-convo" } }), ); await login(page); await page.goto(postPath);
await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "open", { timeout: 10000 }); });
test("renders follow-required action when joinRule is followedByOwner and viewer doesn't follow", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ joinRule: "followedByOwner" }), ); await login(page); await page.goto(postPath);
const action = page.locator('[data-testid="join-link-embed-action"]'); await expect(action).toHaveAttribute( "data-teststate", "follow-required", { timeout: 10000, }, ); await expect(action).toBeDisabled(); });
test("renders requested action when viewer has already requested", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ viewer: { requestedAt: "2026-06-01T00:00:00Z" }, }), ); await login(page); await page.goto(postPath);
await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "requested", { timeout: 10000 }); }); });
test.describe("click behaviors", () => { test("clicking copy writes the invite URL to the clipboard and shows a toast", async ({ page, browserName, }) => { // The copy action only shows when the invite points at the current chat, // which only happens inside chatDetail. Render an invite embed in a DM // where the convo id matches. const mockServer = new MockServer(); const alice = createProfile({ did: "did:plc:alice1", handle: "alice.bsky.social", displayName: "Alice", }); const convo = createConvo({ id: "convo-1", otherMember: alice }); mockServer.addConvos([convo]); mockServer.addConvoMessages("convo-1", [ { $type: "chat.bsky.convo.defs#messageView", id: "msg-1", rev: "rev-msg-1", text: "", facets: [], sender: { did: alice.did }, sentAt: "2025-01-15T12:00:00.000Z", reactions: [], embed: { $type: "chat.bsky.embed.joinLink#view", joinLinkPreview: makeJoinLinkPreview({ convo: { id: "convo-1" }, }), }, }, ]); await mockServer.setup(page);
if (browserName === "chromium") { await page .context() .grantPermissions(["clipboard-read", "clipboard-write"]); }
await login(page); await page.goto("/messages/convo-1");
await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "copy", { timeout: 10000 }); await page.locator('[data-testid="join-link-embed-action"]').click();
await expect(page.locator('[data-testid="toast"]')).toContainText( "Copied to clipboard", );
if (browserName === "chromium") { const clipboardText = await page.evaluate(() => navigator.clipboard.readText(), ); expect(clipboardText).toBe("https://bsky.app/chat/abcd1234"); } });
test("clicking open navigates to the chat", async ({ page }) => { const otherProfile = createProfile({ did: "did:plc:other1", handle: "other.bsky.social", displayName: "Other", }); const otherConvo = createConvo({ id: "other-convo", otherMember: otherProfile, }); const mockServer = new MockServer(); mockServer.addConvos([otherConvo]); mockServer.addConvoMessages("other-convo", []); await setupInvitePostThread( page, makeJoinLinkPreview({ convo: { id: "other-convo" } }), { mockServer }, ); await login(page); await page.goto(postPath);
await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "open", { timeout: 10000 }); await page.locator('[data-testid="join-link-embed-action"]').click();
await expect(page).toHaveURL(/\/messages\/other-convo$/, { timeout: 10000, }); await expect(page.locator("#chat-detail-view")).toBeVisible(); });
test("clicking request opens the dialog and confirming shows a success toast", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ requireApproval: true }), ); await login(page); await page.goto(postPath);
await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "request", { timeout: 10000 }); await page.locator('[data-testid="join-link-embed-action"]').click();
await expect( page.locator('[data-testid="modal-confirm-button"]'), ).toBeVisible(); await page.locator('[data-testid="modal-confirm-button"]').click();
await expect(page.locator('[data-testid="toast"]')).toContainText( "Request sent", ); await expect( page.locator('[data-testid="modal-confirm-button"]'), ).toHaveCount(0); });
test("clicking requested shows a 'request pending' toast", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ viewer: { requestedAt: "2026-06-01T00:00:00Z" }, }), ); await login(page); await page.goto(postPath);
const action = page.locator('[data-testid="join-link-embed-action"]'); await expect(action).toHaveAttribute("data-teststate", "requested", { timeout: 10000, }); await action.click();
await expect(page.locator('[data-testid="modal-title"]')).toContainText( "Request pending", ); }); });
test.describe("join confirmation", () => { test("on success, the action flips to the requested state", async ({ page, }) => { await setupInvitePostThread( page, makeJoinLinkPreview({ requireApproval: true }), ); await login(page); await page.goto(postPath);
const action = page.locator('[data-testid="join-link-embed-action"]'); await expect(action).toHaveAttribute("data-teststate", "request", { timeout: 10000, }); await action.click();
await page.locator('[data-testid="modal-confirm-button"]').click();
await expect(action).toHaveAttribute("data-teststate", "requested", { timeout: 10000, }); });
test("on open-join success, navigates to the joined chat", async ({ page, }) => { const owner = createProfile({ did: "did:plc:owner", handle: "owner.bsky.social", displayName: "Owner", }); const joinedConvo = createConvo({ id: "joined-convo", otherMember: owner, }); const mockServer = new MockServer(); mockServer.addConvos([joinedConvo]); mockServer.addConvoMessages("joined-convo", []); mockServer.setJoinLinkJoinedConvo("abcd1234", joinedConvo); await setupInvitePostThread(page, makeJoinLinkPreview(), { mockServer }); await login(page); await page.goto(postPath);
const action = page.locator('[data-testid="join-link-embed-action"]'); await expect(action).toHaveAttribute("data-teststate", "join", { timeout: 10000, }); await action.click(); await page.locator('[data-testid="modal-confirm-button"]').click();
await expect(page).toHaveURL(/\/messages\/joined-convo$/, { timeout: 10000, }); await expect(page.locator("#chat-detail-view")).toBeVisible(); });
test("on failure, the dialog stays open and an error toast appears", async ({ page, }) => { const mockServer = new MockServer(); mockServer.failJoinLinkRequest("abcd1234"); await setupInvitePostThread(page, makeJoinLinkPreview(), { mockServer }); await login(page); await page.goto(postPath);
const action = page.locator('[data-testid="join-link-embed-action"]'); await expect(action).toHaveAttribute("data-teststate", "join", { timeout: 10000, }); await action.click();
await page.locator('[data-testid="modal-confirm-button"]').click();
await expect(page.locator('[data-testid="toast"]')).toContainText( "Could not send join request", ); await expect( page.locator('[data-testid="modal-confirm-button"]'), ).toBeVisible(); });
test("cancel button closes the dialog without sending the request", async ({ page, }) => { await setupInvitePostThread(page, makeJoinLinkPreview()); await login(page); await page.goto(postPath);
await page.locator('[data-testid="join-link-embed-action"]').click(); await expect( page.locator('[data-testid="modal-confirm-button"]'), ).toBeVisible();
await page.locator('[data-testid="modal-cancel-button"]').click();
await expect( page.locator('[data-testid="modal-confirm-button"]'), ).toHaveCount(0); await expect( page.locator('[data-testid="join-link-embed-action"]'), ).toHaveAttribute("data-teststate", "join"); });
test("clicking the backdrop closes the dialog", async ({ page }) => { await setupInvitePostThread(page, makeJoinLinkPreview()); await login(page); await page.goto(postPath);
await page.locator('[data-testid="join-link-embed-action"]').click(); const dialog = page.locator('[data-testid="join-chat-modal"]'); await expect(dialog).toBeVisible();
// Click at the very top-left corner (outside the dialog content) await dialog.click({ position: { x: 2, y: 2 } });
await expect(dialog).toHaveCount(0); }); });});