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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495import { test, expect } from "../../base.js";import { login } from "../../helpers.js";import { userProfile } from "../../testData.js";import { MockServer } from "../../mockServer.js";import { createPost } from "../../../shared/factories.js";
test.describe("Edit threadgate flow", () => { const ownPostUri = `at://${userProfile.did}/app.bsky.feed.post/own1`;
function makeOwnPost(overrides = {}) { return createPost({ uri: ownPostUri, text: "My own post", authorHandle: userProfile.handle, authorDisplayName: userProfile.displayName, ...overrides, }); }
test("edits the gate to Nobody from the post menu, preserving hiddenReplies", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addPosts([makeOwnPost()]); // A prior record written elsewhere (with hidden replies) that the edit // must carry forward const hiddenReplies = [`at://${userProfile.did}/app.bsky.feed.post/reply1`]; mockServer.threadgateRecords.set("own1", { cid: "bafypriorgate", value: { $type: "app.bsky.feed.threadgate", post: ownPostUri, hiddenReplies, createdAt: "2024-01-01T00:00:00.000Z", }, }); await mockServer.setup(page);
await login(page); await page.goto(`/profile/${userProfile.handle}/post/own1`);
const largePost = page.locator('[data-testid="large-post"]'); await expect(largePost).toBeVisible({ timeout: 10000 }); const badge = page.locator('[data-testid="who-can-reply-badge"]'); await expect(badge).toHaveAttribute("data-teststate", "everybody");
await largePost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); await expect( dialog.locator('[data-testid="interaction-settings-reply-anyone"]'), ).toBeChecked();
await dialog .locator('[data-testid="interaction-settings-reply-nobody"]') .click(); await dialog.locator('[data-testid="interaction-settings-save"]').click();
await expect(dialog).not.toBeVisible({ timeout: 5000 }); await expect(badge).toHaveAttribute("data-teststate", "nobody");
expect(mockServer.putThreadgateCalls.length).toBe(1); const body = mockServer.putThreadgateCalls[0]; expect(body.collection).toBe("app.bsky.feed.threadgate"); expect(body.rkey).toBe("own1"); expect(body.swapRecord).toBe("bafypriorgate"); expect(body.record.allow).toEqual([]); expect(body.record.hiddenReplies).toEqual(hiddenReplies); });
test("edits back to Anyone omitting the allow key, from the badge", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addPosts([ makeOwnPost({ threadgate: { uri: ownPostUri.replace("feed.post", "feed.threadgate"), cid: "bafypriorgate", record: { $type: "app.bsky.feed.threadgate", post: ownPostUri, allow: [], }, lists: [], }, }), ]); mockServer.threadgateRecords.set("own1", { cid: "bafypriorgate", value: { $type: "app.bsky.feed.threadgate", post: ownPostUri, allow: [], }, }); await mockServer.setup(page);
await login(page); await page.goto(`/profile/${userProfile.handle}/post/own1`);
const badge = page.locator('[data-testid="who-can-reply-badge"]'); await expect(badge).toHaveAttribute("data-teststate", "nobody", { timeout: 10000, }); await expect(badge).toHaveClass(/who-can-reply-badge-link/);
await badge.click(); const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); await expect( dialog.locator('[data-testid="interaction-settings-reply-nobody"]'), ).toBeChecked();
await dialog .locator('[data-testid="interaction-settings-reply-anyone"]') .click(); await dialog.locator('[data-testid="interaction-settings-save"]').click();
await expect(dialog).not.toBeVisible({ timeout: 5000 }); await expect(badge).toHaveAttribute("data-teststate", "everybody");
expect(mockServer.putThreadgateCalls.length).toBe(1); const body = mockServer.putThreadgateCalls[0]; expect("allow" in body.record).toBe(false); });
test("disables quote posts from the dialog without touching the threadgate", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addPosts([makeOwnPost()]); await mockServer.setup(page);
await login(page); await page.goto(`/profile/${userProfile.handle}/post/own1`);
const largePost = page.locator('[data-testid="large-post"]'); await expect(largePost).toBeVisible({ timeout: 10000 }); await largePost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); const quoteToggle = dialog.locator( '[data-testid="interaction-settings-quote-posts"]', ); await expect(quoteToggle).toHaveAttribute("checked", ""); await quoteToggle.click(); await expect(quoteToggle).not.toHaveAttribute("checked", ""); await dialog.locator('[data-testid="interaction-settings-save"]').click(); await expect(dialog).not.toBeVisible({ timeout: 5000 });
expect(mockServer.putThreadgateCalls.length).toBe(0); expect(mockServer.putPostgateCalls.length).toBe(1); const body = mockServer.putPostgateCalls[0]; expect(body.collection).toBe("app.bsky.feed.postgate"); expect(body.rkey).toBe("own1"); expect(body.record.embeddingRules).toEqual([ { $type: "app.bsky.feed.postgate#disableRule" }, ]);
// Reopening the dialog reflects the optimistic viewer state await largePost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click(); await expect( dialog.locator('[data-testid="interaction-settings-quote-posts"]'), ).not.toHaveAttribute("checked", ""); });
test("does not offer editing on another user's post", async ({ page }) => { const otherDid = "did:plc:other"; const otherPost = createPost({ uri: `at://${otherDid}/app.bsky.feed.post/notmine`, text: "Not my post", authorHandle: "other.test", authorDisplayName: "Other User", }); const mockServer = new MockServer(); mockServer.addPosts([otherPost]); await mockServer.setup(page);
await login(page); await page.goto("/profile/other.test/post/notmine");
const largePost = page.locator('[data-testid="large-post"]'); await expect(largePost).toBeVisible({ timeout: 10000 });
const badge = page.locator('[data-testid="who-can-reply-badge"]'); await expect(badge).not.toHaveClass(/who-can-reply-badge-link/); await badge.click(); await expect( page.locator('[data-testid="who-can-reply-modal"]'), ).toBeVisible(); await page.locator('[data-testid="modal-primary-button"]').click();
await largePost.locator('[data-testid="post-action-more"]').click(); await expect( page.locator('[data-testid="menu-action-interaction-settings"]'), ).not.toBeAttached(); });
test("edits quote settings on an own reply in another user's thread", async ({ page, }) => { const otherDid = "did:plc:other"; const rootUri = `at://${otherDid}/app.bsky.feed.post/root1`; const allow = [{ $type: "app.bsky.feed.threadgate#mentionRule" }]; const rootPost = createPost({ uri: rootUri, text: "Someone else's thread", authorHandle: "other.test", authorDisplayName: "Other User", replyCount: 1, threadgate: { uri: `at://${otherDid}/app.bsky.feed.threadgate/root1`, cid: "bafyothergate", record: { $type: "app.bsky.feed.threadgate", post: rootUri, allow }, lists: [], }, }); const ownReply = makeOwnPost({ text: "My reply", reply: { root: { uri: rootUri, cid: rootPost.cid }, parent: { uri: rootUri, cid: rootPost.cid }, }, }); const mockServer = new MockServer(); mockServer.addPosts([rootPost, ownReply]); mockServer.setPostThread(rootUri, { $type: "app.bsky.feed.defs#threadViewPost", post: rootPost, parent: null, replies: [ { $type: "app.bsky.feed.defs#threadViewPost", post: ownReply, replies: [], }, ], }); await mockServer.setup(page);
await login(page); await page.goto("/profile/other.test/post/root1");
const replyPost = page .locator("#post-detail-view [data-testid='small-post']") .filter({ hasText: "My reply" }); await expect(replyPost).toBeVisible({ timeout: 10000 }); await replyPost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); await expect( dialog.locator( '[data-testid="interaction-settings-reply-disabled-notice"]', ), ).toHaveAttribute("data-teststate", "readonly"); await expect( dialog.locator('[data-testid="interaction-settings-mention"]'), ).toBeChecked(); await expect( dialog.locator('[data-testid="interaction-settings-reply-nobody"]'), ).toBeDisabled();
await dialog .locator('[data-testid="interaction-settings-quote-posts"]') .click(); await dialog.locator('[data-testid="interaction-settings-save"]').click(); await expect(dialog).not.toBeVisible({ timeout: 5000 });
expect(mockServer.putThreadgateCalls.length).toBe(0); expect(mockServer.putPostgateCalls.length).toBe(1); const body = mockServer.putPostgateCalls[0]; expect(body.rkey).toBe("own1"); expect(body.record.post).toBe(ownPostUri); expect(body.record.embeddingRules).toEqual([ { $type: "app.bsky.feed.postgate#disableRule" }, ]); });
test("still edits quote settings on an own reply whose thread root can't load", async ({ page, }) => { const rootUri = "at://did:plc:other/app.bsky.feed.post/deleted1"; const ownReply = makeOwnPost({ text: "My reply", reply: { root: { uri: rootUri, cid: "bafydeletedroot" }, parent: { uri: rootUri, cid: "bafydeletedroot" }, }, }); const mockServer = new MockServer(); mockServer.addPosts([ownReply]); mockServer.setPostThread(ownPostUri, { $type: "app.bsky.feed.defs#threadViewPost", post: ownReply, parent: { $type: "app.bsky.feed.defs#notFoundPost", uri: rootUri }, replies: [], }); await mockServer.setup(page);
await login(page); await page.goto(`/profile/${userProfile.handle}/post/own1`);
const largePost = page.locator('[data-testid="large-post"]'); await expect(largePost).toBeVisible({ timeout: 10000 }); await largePost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); await expect( dialog.locator( '[data-testid="interaction-settings-reply-disabled-notice"]', ), ).toHaveAttribute("data-teststate", "unavailable"); await expect( dialog.locator('[data-testid="interaction-settings-reply-anyone"]'), ).not.toBeChecked(); await expect( dialog.locator('[data-testid="interaction-settings-reply-anyone"]'), ).toBeDisabled();
await dialog .locator('[data-testid="interaction-settings-quote-posts"]') .click(); await dialog.locator('[data-testid="interaction-settings-save"]').click(); await expect(dialog).not.toBeVisible({ timeout: 5000 });
expect(mockServer.putThreadgateCalls.length).toBe(0); expect(mockServer.putPostgateCalls.length).toBe(1); expect(mockServer.putPostgateCalls[0].rkey).toBe("own1"); });
test("edits the thread root's gate from an own reply in an own thread", async ({ page, }) => { const rootUri = `at://${userProfile.did}/app.bsky.feed.post/root1`; const rootPost = createPost({ uri: rootUri, text: "My thread", authorHandle: userProfile.handle, authorDisplayName: userProfile.displayName, replyCount: 1, }); const ownReply = makeOwnPost({ text: "My reply", reply: { root: { uri: rootUri, cid: rootPost.cid }, parent: { uri: rootUri, cid: rootPost.cid }, }, }); const mockServer = new MockServer(); mockServer.addPosts([rootPost, ownReply]); mockServer.setPostThread(rootUri, { $type: "app.bsky.feed.defs#threadViewPost", post: rootPost, parent: null, replies: [ { $type: "app.bsky.feed.defs#threadViewPost", post: ownReply, replies: [], }, ], }); await mockServer.setup(page);
await login(page); await page.goto(`/profile/${userProfile.handle}/post/root1`);
const replyPost = page .locator("#post-detail-view [data-testid='small-post']") .filter({ hasText: "My reply" }); await expect(replyPost).toBeVisible({ timeout: 10000 }); await replyPost.locator('[data-testid="post-action-more"]').click(); await page .locator('[data-testid="menu-action-interaction-settings"]') .click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); const nobody = dialog.locator( '[data-testid="interaction-settings-reply-nobody"]', ); await expect(nobody).toBeEnabled(); await expect( dialog.locator( '[data-testid="interaction-settings-reply-disabled-notice"]', ), ).not.toBeAttached();
await nobody.click(); await dialog.locator('[data-testid="interaction-settings-save"]').click(); await expect(dialog).not.toBeVisible({ timeout: 5000 });
expect(mockServer.putPostgateCalls.length).toBe(0); expect(mockServer.putThreadgateCalls.length).toBe(1); const body = mockServer.putThreadgateCalls[0]; expect(body.rkey).toBe("root1"); expect(body.record.post).toBe(rootUri); expect(body.record.allow).toEqual([]); });
test("composes a post with a threadgate written in the same batch", async ({ page, }) => { const mockServer = new MockServer(); await mockServer.setup(page);
await login(page); await page.goto("/");
await expect(page.locator("#home-view")).toBeVisible({ timeout: 10000 }); await page.locator('[data-testid="sidebar-compose-button"]').click();
const composer = page.locator("post-composer .post-composer"); await expect(composer).toBeVisible({ timeout: 10000 });
const control = composer.locator( '[data-testid="composer-interaction-settings"]', ); await expect(control).toHaveAttribute("data-teststate", "open"); await control.click();
const dialog = page.locator( '[data-testid="post-interaction-settings-dialog"]', ); await expect(dialog).toBeVisible(); await dialog .locator('[data-testid="interaction-settings-mention"]') .click(); await dialog .locator('[data-testid="interaction-settings-following"]') .click(); await dialog .locator('[data-testid="interaction-settings-quote-posts"]') .click(); await dialog.locator('[data-testid="interaction-settings-save"]').click(); await expect(dialog).not.toBeVisible({ timeout: 5000 }); await expect(control).toHaveAttribute("data-teststate", "limited");
const richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type("A gated post"); await composer.locator('[data-testid="composer-submit-button"]').click(); await expect(composer).not.toBeVisible({ timeout: 10000 });
expect(mockServer.applyWritesCalls.length).toBe(1); const writes = mockServer.applyWritesCalls[0]; expect(writes.map((write) => write.collection)).toEqual([ "app.bsky.feed.post", "app.bsky.feed.threadgate", "app.bsky.feed.postgate", ]); expect(writes[1].rkey).toBe(writes[0].rkey); expect(writes[1].value.allow).toEqual([ { $type: "app.bsky.feed.threadgate#mentionRule" }, { $type: "app.bsky.feed.threadgate#followingRule" }, ]); expect(writes[2].rkey).toBe(writes[0].rkey); expect(writes[2].value.embeddingRules).toEqual([ { $type: "app.bsky.feed.postgate#disableRule" }, ]); });});