import { 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, }); }); });