Skip to main content
Glama
Southclaws

Storyden

by Southclaws
thread.spec.ts14.7 kB
import { Page, expect, test } from "@playwright/test"; const PASSWORD = "TestPassword123!"; async function dismissOnboarding(page: Page) { const skipButton = page.getByRole("button", { name: "Skip" }); if (await skipButton.isVisible({ timeout: 1000 }).catch(() => false)) { await skipButton.click(); } } async function registerUser(page: Page, username: string) { await page.goto("/register"); await page.getByRole("textbox", { name: "username" }).fill(username); await page.getByRole("textbox", { name: "password" }).fill(PASSWORD); await page.getByRole("button", { name: "Register" }).click(); await expect(page).toHaveURL("/", { timeout: 10000 }); await expect( page.getByRole("button", { name: "Account menu" }), ).toBeVisible(); await dismissOnboarding(page); } async function logout(page: Page) { await page.getByRole("button", { name: "Account menu" }).click(); await page.getByRole("link", { name: "Logout" }).click(); await expect(page.getByRole("link", { name: "Login" })).toBeVisible(); } async function login(page: Page, username: string) { await page.goto("/login"); await page.getByRole("textbox", { name: "username" }).fill(username); await page.getByRole("textbox", { name: "password" }).fill(PASSWORD); await page.getByRole("button", { name: "Login" }).click(); await expect(page).toHaveURL("/", { timeout: 10000 }); await expect( page.getByRole("button", { name: "Account menu" }), ).toBeVisible(); await dismissOnboarding(page); } async function createThread( page: Page, title: string, body: string, ): Promise<string> { await page.getByRole("link", { name: "Post" }).click(); await expect(page).toHaveURL("/new", { timeout: 5000 }); await page.locator("#title-input").fill(title); const editor = page.locator(".ProseMirror").first(); await editor.click(); await editor.fill(body); await page.getByRole("button", { name: "Post" }).click(); await expect(page).toHaveURL(/\/t\//, { timeout: 10000 }); await dismissOnboarding(page); const url = page.url(); return url; } async function postReply(page: Page, body: string) { const replyForm = page.locator("form").filter({ hasText: "Reply to" }); const replyEditor = replyForm.locator(".ProseMirror[contenteditable='true']"); await replyEditor.click(); await replyEditor.fill(body); await replyForm.getByRole("button", { name: "Post" }).click(); await page .locator("li") .filter({ hasText: body }) .waitFor({ timeout: 10000 }); } async function navigateToThread(page: Page, threadUrl: string) { await page.goto(threadUrl); await dismissOnboarding(page); } test.describe("Thread Creation", () => { test("should create a thread with title and body", async ({ page }) => { await registerUser(page, "thread-creator-01"); const threadUrl = await createThread( page, "My First Thread", "This is the body of my first thread.", ); await expect( page.getByRole("heading", { name: "My First Thread" }), ).toBeVisible(); await expect( page.locator("main").getByText("This is the body of my first thread."), ).toBeVisible(); expect(threadUrl).toMatch(/\/t\//); }); }); test.describe("Thread Replies", () => { test("should reply to a thread from a different account", async ({ page, }) => { await registerUser(page, "thread-author-01"); const threadUrl = await createThread( page, "Thread for Replies", "This thread will receive replies.", ); await logout(page); await registerUser(page, "replier-01"); await navigateToThread(page, threadUrl); await postReply(page, "This is a reply from another user."); await expect( page .locator("li") .filter({ hasText: "This is a reply from another user." }), ).toBeVisible(); }); test("should reply to a specific reply", async ({ page }) => { await registerUser(page, "thread-author-02"); const threadUrl = await createThread( page, "Thread for Reply Threading", "This thread tests reply-to-reply functionality.", ); await postReply(page, "First reply to the thread."); await logout(page); await registerUser(page, "replier-02"); await navigateToThread(page, threadUrl); const firstReply = page .locator("li") .filter({ hasText: "First reply to the thread." }); await firstReply.getByRole("button", { name: "Reply to this" }).click(); await expect(page.getByText("Replying to")).toBeVisible(); const replyBox = page .locator("form") .filter({ has: page.getByRole("button", { name: "Post" }) }) .last(); const replyEditor = replyBox.locator( ".ProseMirror[contenteditable='true']", ); await replyEditor.click(); await replyEditor.fill("This is a reply to the first reply."); await replyBox.getByRole("button", { name: "Post" }).click(); const newReply = page .locator("li") .filter({ hasText: "This is a reply to the first reply." }); await expect(newReply).toBeVisible({ timeout: 10000 }); await expect( newReply.locator("a").filter({ hasText: "First reply to the thread." }), ).toBeVisible(); }); test("should clear reply-to selection", async ({ page }) => { await registerUser(page, "thread-author-03"); const threadUrl = await createThread( page, "Thread for Clear Reply-To", "Testing the clear reply-to button.", ); await postReply(page, "A reply to set as target."); await navigateToThread(page, threadUrl); const reply = page .locator("li") .filter({ hasText: "A reply to set as target." }); await reply.getByRole("button", { name: "Reply to this" }).click(); await expect(page.getByText("Replying to")).toBeVisible(); await page.getByRole("button", { name: "Clear reply-to" }).click(); await expect(page.getByText("Replying to")).not.toBeVisible(); }); }); test.describe("Thread Notifications", () => { test("should show notification when someone replies to your thread", async ({ page, }) => { await registerUser(page, "notification-author-01"); const threadUrl = await createThread( page, "Thread for Notification Test", "Waiting for someone to reply.", ); await logout(page); await registerUser(page, "notification-replier-01"); await navigateToThread(page, threadUrl); await postReply(page, "A reply that should trigger a notification."); await logout(page); await login(page, "notification-author-01"); await expect( page.getByRole("button", { name: "Notifications" }), ).toBeVisible(); await page.getByRole("button", { name: "Notifications" }).click(); await expect(page.getByText("notification-replier-01")).toBeVisible({ timeout: 10000, }); await expect(page.getByText("replied to")).toBeVisible(); }); }); test.describe("Thread Reactions", () => { test("should add a reaction to a reply", async ({ page }) => { await registerUser(page, "reaction-author-01"); const threadUrl = await createThread( page, "Thread for Reaction Test", "This thread will test reactions.", ); await postReply(page, "A reply to react to."); const reply = page .locator("li") .filter({ hasText: "A reply to react to." }); const reactionButton = reply.getByRole("button", { name: "Add reaction" }); await expect(reactionButton).toBeVisible({ timeout: 10000 }); await reactionButton.click(); const emojiPicker = page.locator(".EmojiPickerReact"); await emojiPicker.waitFor({ timeout: 5000 }); await emojiPicker.locator("button.epr-emoji").first().click(); await expect( reply.locator("button").filter({ hasText: /^\p{Emoji}.*\d$/u }), ).toBeVisible({ timeout: 5000 }); }); }); test.describe("Thread Editing", () => { test("should edit thread title and body", async ({ page }) => { await registerUser(page, "edit-author-01"); const threadUrl = await createThread( page, "Original Thread Title", "Original thread body content.", ); await page.goto(threadUrl + "?edit=true"); await dismissOnboarding(page); const titleInput = page .locator("main span[contenteditable='true']") .first(); await expect(titleInput).toBeVisible({ timeout: 5000 }); await titleInput.click(); await page.keyboard.press("Meta+a"); await page.keyboard.type("Updated Thread Title"); const editor = page .locator("main .ProseMirror[contenteditable='true']") .first(); await editor.click(); await page.keyboard.press("Meta+a"); await editor.fill("Updated thread body content."); await page.getByRole("button", { name: "Save" }).click(); await expect( page.getByRole("heading", { name: "Updated Thread Title" }), ).toBeVisible({ timeout: 10000 }); await expect( page.locator("main").getByText("Updated thread body content."), ).toBeVisible(); }); test("should discard thread edits", async ({ page }) => { await registerUser(page, "edit-author-02"); const threadUrl = await createThread( page, "Thread to Not Edit", "Original content that should remain.", ); await page.goto(threadUrl + "?edit=true"); await dismissOnboarding(page); const titleInput = page .locator("main span[contenteditable='true']") .first(); await titleInput.click(); await page.keyboard.press("Meta+a"); await page.keyboard.type("This change should be discarded"); await page.getByRole("button", { name: "Discard" }).click(); await expect( page.getByRole("heading", { name: "Thread to Not Edit" }), ).toBeVisible(); await expect( page.locator("main").getByText("Original content that should remain."), ).toBeVisible(); }); }); test.describe("Reply Editing", () => { test("should edit a reply", async ({ page }) => { await registerUser(page, "reply-edit-author-01"); await createThread( page, "Thread for Reply Editing", "This thread tests reply editing.", ); await postReply(page, "Original reply content."); const reply = page .locator("li") .filter({ hasText: "Original reply content." }); const moreButton = reply.getByRole("button", { name: "More options" }); await expect(moreButton).toBeVisible({ timeout: 10000 }); await moreButton.click(); const editMenuItem = page.getByRole("menuitem", { name: /Edit/ }); await expect(editMenuItem).toBeVisible({ timeout: 5000 }); await editMenuItem.click(); const saveButton = page.getByRole("button", { name: "Save" }); await expect(saveButton).toBeVisible({ timeout: 10000 }); const replyEditor = page.locator("li .ProseMirror[contenteditable='true']"); await expect(replyEditor).toBeVisible({ timeout: 5000 }); await replyEditor.click(); await page.keyboard.press("Meta+a"); await replyEditor.fill("Updated reply content."); await saveButton.click(); await expect( page.locator("li").filter({ hasText: "Updated reply content." }), ).toBeVisible({ timeout: 10000 }); }); test("should discard reply edits", async ({ page }) => { await registerUser(page, "reply-edit-author-02"); await createThread( page, "Thread for Reply Discard", "Testing reply edit discard.", ); await postReply(page, "Reply that should not change."); const reply = page .locator("li") .filter({ hasText: "Reply that should not change." }); const moreButton = reply.getByRole("button", { name: "More options" }); await expect(moreButton).toBeVisible({ timeout: 10000 }); await moreButton.click(); const editMenuItem = page.getByRole("menuitem", { name: /Edit/ }); await expect(editMenuItem).toBeVisible({ timeout: 5000 }); await editMenuItem.click(); const discardButton = page.getByRole("button", { name: "Discard" }); await expect(discardButton).toBeVisible({ timeout: 10000 }); const replyEditor = page.locator("li .ProseMirror[contenteditable='true']"); await expect(replyEditor).toBeVisible({ timeout: 5000 }); await replyEditor.click(); await page.keyboard.press("Meta+a"); await replyEditor.fill("This should be discarded."); await discardButton.click(); await expect( page.locator("li").filter({ hasText: "Reply that should not change." }), ).toBeVisible(); }); }); test.describe("Thread Pagination", () => { test("should paginate replies when exceeding page size", async ({ page }) => { test.setTimeout(300000); await registerUser(page, "pagination-author-01"); const threadUrl = await createThread( page, "Thread for Pagination Test", "This thread will have many replies.", ); for (let i = 1; i <= 53; i++) { const replyForm = page .locator("form") .filter({ has: page.getByRole("button", { name: "Post" }) }) .last(); const replyEditor = replyForm.locator( ".ProseMirror[contenteditable='true']", ); await replyEditor.click(); await replyEditor.fill(`Reply number ${i} for pagination test.`); await replyForm.getByRole("button", { name: "Post" }).click(); await page.waitForTimeout(200); } await navigateToThread(page, threadUrl); await expect( page .locator("li") .filter({ hasText: "Reply number 1 for pagination test." }), ).toBeVisible(); await expect( page .locator("li") .filter({ hasText: "Reply number 50 for pagination test." }), ).toBeVisible(); await expect( page .locator("li") .filter({ hasText: "Reply number 51 for pagination test." }), ).not.toBeVisible(); await expect(page.getByRole("link", { name: "2" }).first()).toBeVisible(); await page.getByRole("link", { name: "2" }).first().click(); await expect(page).toHaveURL(/page=2/, { timeout: 5000 }); await expect( page .locator("li") .filter({ hasText: "Reply number 51 for pagination test." }), ).toBeVisible(); await expect( page .locator("li") .filter({ hasText: "Reply number 52 for pagination test." }), ).toBeVisible(); await expect( page .locator("li") .filter({ hasText: "Reply number 53 for pagination test." }), ).toBeVisible(); await expect( page .locator("li") .filter({ hasText: "Reply number 1 for pagination test." }), ).not.toBeVisible(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server