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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530import { test, expect } from "../../base.js";import { login } from "../../helpers.js";import { userProfile } from "../../testData.js";import { MockServer } from "../../mockServer.js";import { createPost, createProfile, createFeedGenerator, createList, createStarterPack,} from "../../../shared/factories.js";
test.describe("Create post flow", () => { test("should show created post on profile after composing from home", async ({ page, }) => { const mockServer = new MockServer(); await mockServer.setup(page);
const postText = "Hello world, this is my new post!";
await login(page); await page.goto("/");
// Click the sidebar compose button on home view const homeView = page.locator("#home-view"); await expect(homeView).toBeVisible({ timeout: 10000 }); await page.locator('[data-testid="sidebar-compose-button"]').click();
// Wait for the post composer dialog to appear const composer = page.locator("post-composer .post-composer"); await expect(composer).toBeVisible({ timeout: 10000 });
// Type the post text into the rich text input const richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type(postText);
// Click the Post button await composer.locator('[data-testid="composer-submit-button"]').click();
// Wait for the composer to close await expect(composer).not.toBeVisible({ timeout: 10000 });
// Navigate to own profile await page.goto(`/profile/${userProfile.did}`);
const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText(postText); });
test("should create post with image upload and alt text", async ({ page, }) => { const mockServer = new MockServer(); await mockServer.setup(page);
await login(page); await page.goto("/");
// Open composer const homeView = page.locator("#home-view"); await expect(homeView).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 });
// Type post text const richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type("Check out this photo!");
// Upload a 1x1 pixel PNG via file input const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const fileInput = composer.locator(".media-picker-input"); await fileInput.setInputFiles({ name: "test-image.png", mimeType: "image/png", buffer: Buffer.from(pngBase64, "base64"), });
// Verify image preview appears await expect(composer.locator(".image-preview-item")).toHaveCount(1, { timeout: 10000, });
// Alt indicator should show no alt text initially await expect(composer.locator(".alt-indicator.no-alt")).toBeVisible();
// Click the image to open alt text dialog await composer.locator(".image-preview-item img").click();
// Wait for the alt text dialog to appear const altDialog = page.locator( "image-alt-text-dialog .image-alt-text-dialog", ); await expect(altDialog).toBeVisible({ timeout: 10000 });
// Type alt text await altDialog .locator(".image-alt-text-dialog-textarea") .fill("A beautiful sunset over the mountains");
// Save alt text await altDialog.locator('[data-testid="alt-text-save"]').click();
// Verify alt indicator now shows has-alt await expect(composer.locator(".alt-indicator.has-alt")).toBeVisible();
// Click Post await composer.locator('[data-testid="composer-submit-button"]').click();
// Wait for the composer to close await expect(composer).not.toBeVisible({ timeout: 10000 });
// Navigate to profile and verify post appears await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText("Check out this photo!");
// Verify the image embed rendered with alt text const postImages = profileView.locator('[data-testid="post-images"]'); await expect(postImages).toBeVisible(); await expect(postImages.locator(".alt-indicator")).toContainText("ALT"); await expect(postImages.locator("img.post-image")).toHaveAttribute( "alt", "A beautiful sunset over the mountains", ); });
test("should create post with external link preview embed", async ({ page, }) => { const mockServer = new MockServer(); mockServer.setExternalLinkCard("https://example.com/article", { title: "Example Article Title", description: "This is a great article about testing", }); await mockServer.setup(page);
await login(page); await page.goto("/");
// Open composer const homeView = page.locator("#home-view"); await expect(homeView).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 });
// Type text with URL and a trailing space to trigger link detection const richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type("Great article https://example.com/article ");
// Wait for the external link card preview to appear await expect(composer.locator('[data-testid="external-link"]')).toBeVisible( { timeout: 10000 }, ); await expect( composer.locator('[data-testid="external-link-title"]'), ).toContainText("Example Article Title"); await expect( composer.locator('[data-testid="external-link-description"]'), ).toContainText("This is a great article about testing");
// Click Post await composer.locator('[data-testid="composer-submit-button"]').click();
// Wait for the composer to close await expect(composer).not.toBeVisible({ timeout: 10000 });
// Navigate to profile and verify post appears with the link embed await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText("Great article"); await expect( profileView.locator('[data-testid="external-link-title"]'), ).toContainText("Example Article Title"); });
test("should create post with quote post embed by pasting bsky.app URL", async ({ page, }) => { const mockServer = new MockServer(); const originalPost = createPost({ uri: "at://did:plc:author1/app.bsky.feed.post/post1", text: "Original post to be quoted", authorHandle: "author1.bsky.social", authorDisplayName: "Author One", }); mockServer.addPosts([originalPost]); await mockServer.setup(page);
await login(page); await page.goto("/");
// Open composer const homeView = page.locator("#home-view"); await expect(homeView).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 });
// Type text with a bsky.app post URL and a trailing space to trigger quote detection const richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type( "My thoughts: https://bsky.app/profile/author1.bsky.social/post/post1 ", );
// Wait for the quoted post embed preview to appear await expect(composer.locator(".quoted-post")).toBeVisible({ timeout: 10000, }); await expect(composer.locator(".quoted-post")).toContainText( "Original post to be quoted", );
// Click Post await composer.locator('[data-testid="composer-submit-button"]').click();
// Wait for the composer to close await expect(composer).not.toBeVisible({ timeout: 10000 });
// Navigate to profile and verify post appears with the quote embed await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText("My thoughts:"); await expect(profileView).toContainText("Original post to be quoted"); });
test("should create post with feed generator embed by pasting feed URL", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addFeedGenerators([ createFeedGenerator({ uri: "at://did:plc:creator1/app.bsky.feed.generator/cool-feed", displayName: "Cool Feed", creatorHandle: "creator1.bsky.social", }), ]); await mockServer.setup(page);
await login(page); await page.goto("/");
const homeView = page.locator("#home-view"); await expect(homeView).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 richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type( "Try this: https://bsky.app/profile/creator1.bsky.social/feed/cool-feed ", );
await expect(composer.locator(".feed-generator-embed")).toBeVisible({ timeout: 10000, }); await expect(composer.locator(".feed-generator-embed")).toContainText( "Cool Feed", );
await composer.locator('[data-testid="composer-submit-button"]').click(); await expect(composer).not.toBeVisible({ timeout: 10000 });
await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText("Try this:"); await expect(profileView.locator(".feed-generator-embed")).toContainText( "Cool Feed", ); });
test("should create post with list embed by pasting list URL", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addLists([ createList({ uri: "at://did:plc:creator1/app.bsky.graph.list/cool-list", name: "Cool List", creatorHandle: "creator1.bsky.social", }), ]); await mockServer.setup(page);
await login(page); await page.goto("/");
const homeView = page.locator("#home-view"); await expect(homeView).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 richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type( "Follow these folks: https://bsky.app/profile/creator1.bsky.social/lists/cool-list ", );
await expect(composer.locator(".list-embed")).toBeVisible({ timeout: 10000, }); await expect(composer.locator(".list-embed")).toContainText("Cool List");
await composer.locator('[data-testid="composer-submit-button"]').click(); await expect(composer).not.toBeVisible({ timeout: 10000 });
await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView.locator(".list-embed")).toContainText("Cool List"); });
test("should create post with starter pack embed by pasting starter pack URL", async ({ page, }) => { const mockServer = new MockServer(); mockServer.addStarterPacks([ createStarterPack({ uri: "at://did:plc:creator1/app.bsky.graph.starterpack/cool-pack", name: "Cool Pack", creatorHandle: "creator1.bsky.social", description: "People to follow", }), ]); await mockServer.setup(page);
await login(page); await page.goto("/");
const homeView = page.locator("#home-view"); await expect(homeView).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 richTextInput = composer.locator(".rich-text-input"); await richTextInput.click(); await richTextInput.type( "Great starting point: https://bsky.app/starter-pack/creator1.bsky.social/cool-pack ", );
await expect(composer.locator(".starter-pack-embed")).toBeVisible({ timeout: 10000, }); await expect(composer.locator(".starter-pack-embed")).toContainText( "Cool Pack", );
await composer.locator('[data-testid="composer-submit-button"]').click(); await expect(composer).not.toBeVisible({ timeout: 10000 });
await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView.locator(".starter-pack-embed")).toContainText( "Cool Pack", ); });
test("should create post with mention and hashtag facet resolution", async ({ page, }) => { const mockServer = new MockServer(); const mentionedUser = createProfile({ did: "did:plc:alice123", handle: "alice.bsky.social", displayName: "Alice", }); mockServer.addTypeaheadProfiles([mentionedUser]); mockServer.addProfile(mentionedUser); await mockServer.setup(page);
await login(page); await page.goto("/");
// Open composer const homeView = page.locator("#home-view"); await expect(homeView).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 richTextInput = composer.locator(".rich-text-input"); await richTextInput.click();
// Type text with a mention to trigger typeahead await richTextInput.type("Hello @ali");
// Wait for mention typeahead to appear const typeahead = page.locator(".mention-typeahead"); await expect(typeahead).toBeVisible({ timeout: 10000 }); await expect(page.locator(".mention-suggestion")).toHaveCount(1); await expect(page.locator(".mention-suggestion-handle")).toContainText( "@alice.bsky.social", );
// Select the mention via keyboard await page.keyboard.press("Enter");
// Typeahead should close after selection await expect(typeahead).not.toBeVisible({ timeout: 5000 });
// Wait for cursor repositioning after mention selection (uses setTimeout) await page.waitForTimeout(50);
// Continue typing a hashtag (use keyboard to avoid refocusing the element) await page.keyboard.type(" loves #testing ");
// Click Post await composer.locator('[data-testid="composer-submit-button"]').click();
// Wait for the composer to close await expect(composer).not.toBeVisible({ timeout: 10000 });
// Navigate to profile and verify post appears with mention and hashtag await page.goto(`/profile/${userProfile.did}`); const profileView = page.locator("#profile-view"); await expect(profileView.locator('[data-testid="feed-item"]')).toHaveCount( 1, { timeout: 10000 }, ); await expect(profileView).toContainText("@alice.bsky.social"); await expect(profileView).toContainText("#testing"); });
test("media picker is disabled once 4 images are selected", async ({ page, }) => { const mockServer = new MockServer(); await mockServer.setup(page);
await login(page); await page.goto("/intent/compose");
const composer = page.locator("post-composer .post-composer"); await expect(composer).toBeVisible({ timeout: 10000 });
const pickerButton = composer.locator(".image-picker-button"); await expect(pickerButton).toBeVisible(); await expect(pickerButton).toBeEnabled();
const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const mediaInput = composer.locator(".media-picker-input"); await mediaInput.setInputFiles( [0, 1, 2, 3].map((index) => ({ name: `tiny-${index}.png`, mimeType: "image/png", buffer: Buffer.from(pngBase64, "base64"), })), ); await expect(composer.locator(".image-preview-item")).toHaveCount(4, { timeout: 10000, }); await expect(pickerButton).toBeDisabled();
await composer.locator(".image-preview-remove-button").first().click(); await expect(pickerButton).toBeEnabled(); });
test("rejects unsupported video file types with a toast", async ({ page, }) => { const mockServer = new MockServer(); await mockServer.setup(page);
await login(page); await page.goto("/intent/compose");
const composer = page.locator("post-composer .post-composer"); await expect(composer).toBeVisible({ timeout: 10000 });
const videoInput = composer.locator(".media-picker-input"); await videoInput.setInputFiles({ name: "not-a-video.txt", mimeType: "text/plain", buffer: Buffer.from("nope"), });
await expect(composer.locator(".post-composer-video-preview")).toHaveCount( 0, ); await expect(page.locator('[data-testid="toast"]')).toBeVisible({ timeout: 5000, }); });});