mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-21 00:46:39 -05:00
feat: adds e2e test for invite functionality (#1846)
This commit is contained in:
@@ -312,7 +312,7 @@ export default function Navigation({
|
||||
{/* User Dropdown */}
|
||||
<div className="hidden lg:ml-6 lg:flex lg:items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild id="userDropdownTrigger">
|
||||
<div tabIndex={0} className="flex cursor-pointer flex-row items-center space-x-5">
|
||||
{session.user.imageUrl ? (
|
||||
<Image
|
||||
@@ -335,7 +335,7 @@ export default function Navigation({
|
||||
<ChevronDownIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuContent className="w-56" id="userDropdownContentWrapper">
|
||||
<DropdownMenuLabel className="cursor-default break-all">
|
||||
<span className="ph-no-capture font-normal">Signed in as </span>
|
||||
{session?.user?.name && session?.user?.name.length > 30 ? (
|
||||
|
||||
@@ -79,15 +79,21 @@ export default function AddMemberModal({
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div>
|
||||
<Label>Full Name</Label>
|
||||
<Label htmlFor="memberNameInput">Full Name</Label>
|
||||
<Input
|
||||
id="memberNameInput"
|
||||
placeholder="e.g. Hans Wurst"
|
||||
{...register("name", { required: true, validate: (value) => value.trim() !== "" })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Email Adress</Label>
|
||||
<Input type="email" placeholder="hans@wurst.com" {...register("email", { required: true })} />
|
||||
<Label htmlFor="memberEmailInput">Email Address</Label>
|
||||
<Input
|
||||
id="memberEmailInput"
|
||||
type="email"
|
||||
placeholder="hans@wurst.com"
|
||||
{...register("email", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
{canDoRoleManagement && <AddMemberRole control={control} />}
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
return (
|
||||
<>
|
||||
{showDeleteButton && (
|
||||
<button onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<button id="deleteMemberButton" onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<TrashIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
)}
|
||||
@@ -109,7 +109,8 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
<button
|
||||
onClick={() => {
|
||||
handleShareInvite();
|
||||
}}>
|
||||
}}
|
||||
id="shareInviteButton">
|
||||
<ShareIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -122,7 +123,8 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
<button
|
||||
onClick={() => {
|
||||
handleResendInvite();
|
||||
}}>
|
||||
}}
|
||||
id="resendInviteButton">
|
||||
<PaperAirplaneIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -36,10 +36,10 @@ const MembersInfo = async ({
|
||||
const allMembers = [...members, ...invites];
|
||||
|
||||
return (
|
||||
<div className="grid-cols-20">
|
||||
<div className="grid-cols-20" id="membersInfoWrapper">
|
||||
{allMembers.map((member) => (
|
||||
<div
|
||||
className="grid-cols-20 grid h-auto w-full content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
|
||||
className="singleMemberInfo grid-cols-20 grid h-auto w-full content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
|
||||
key={member.email}>
|
||||
<div className="h-58 col-span-2 pl-4">
|
||||
{isInvitee(member) ? (
|
||||
|
||||
@@ -43,7 +43,8 @@ export default function ShareInviteModal({ inviteToken, open, setOpen }: ShareIn
|
||||
<p
|
||||
ref={linkTextRef}
|
||||
className="relative mt-3 w-full truncate rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
onClick={() => handleTextSelection()}
|
||||
id="inviteLinkText">
|
||||
{`${window.location.protocol}//${window.location.host}/invite?token=${inviteToken}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
112
apps/web/playwright/team.spec.ts
Normal file
112
apps/web/playwright/team.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { expect, test } from "playwright/test";
|
||||
|
||||
import { login, signUpAndLogin, signupUsingInviteToken, skipOnboarding } from "./utils/helper";
|
||||
import { invites, users } from "./utils/mock";
|
||||
|
||||
test.describe("Invite, accept and remove team member", async () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
const { email, password, name } = users.team[0];
|
||||
let inviteLink: string;
|
||||
|
||||
test("Invite team member", async ({ page }) => {
|
||||
await signUpAndLogin(page, name, email, password);
|
||||
await skipOnboarding(page);
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
|
||||
const dropdownContentWrapper = page.locator("#userDropdownContentWrapper");
|
||||
await expect(dropdownContentWrapper).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Team" }).click();
|
||||
|
||||
// Add member button
|
||||
await expect(page.getByRole("button", { name: "Add Member" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Add Member" }).click();
|
||||
|
||||
// Fill the member name and email form
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
await page.getByLabel("Full Name").fill(invites.addMember.name);
|
||||
|
||||
await expect(page.getByLabel("Email Address")).toBeVisible();
|
||||
await page.getByLabel("Email Address").fill(invites.addMember.email);
|
||||
|
||||
await page.getByRole("button", { name: "Send Invitation", exact: true }).click();
|
||||
});
|
||||
|
||||
test("Copy Invite Link", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
|
||||
const dropdownContentWrapper = page.locator("#userDropdownContentWrapper");
|
||||
await expect(dropdownContentWrapper).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Team" }).click();
|
||||
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
const pendingSpan = lastMemberInfo.locator("span").filter({ hasText: "Pending" });
|
||||
await expect(pendingSpan).toBeVisible();
|
||||
|
||||
const shareInviteButton = page.locator("#shareInviteButton");
|
||||
await expect(shareInviteButton).toBeVisible();
|
||||
|
||||
await shareInviteButton.click();
|
||||
|
||||
const inviteLinkText = page.locator("#inviteLinkText");
|
||||
await expect(inviteLinkText).toBeVisible();
|
||||
|
||||
// invite link text is a paragraph, and we need the text inside it
|
||||
const inviteLinkTextContent = await inviteLinkText.textContent();
|
||||
if (inviteLinkTextContent) {
|
||||
inviteLink = inviteLinkTextContent;
|
||||
}
|
||||
});
|
||||
|
||||
test("Accept Invite", async ({ page }) => {
|
||||
const { email, name, password } = users.team[1];
|
||||
page.goto(inviteLink);
|
||||
|
||||
await page.waitForURL(/\/invite\?token=[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/);
|
||||
|
||||
// Create account button
|
||||
await expect(page.getByRole("link", { name: "Create account" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Create account" }).click();
|
||||
|
||||
await signupUsingInviteToken(page, name, email, password);
|
||||
await skipOnboarding(page);
|
||||
});
|
||||
|
||||
test("Remove Member", async ({ page }) => {
|
||||
await login(page, email, password);
|
||||
|
||||
const dropdownTrigger = page.locator("#userDropdownTrigger");
|
||||
await expect(dropdownTrigger).toBeVisible();
|
||||
await dropdownTrigger.click();
|
||||
|
||||
const dropdownContentWrapper = page.locator("#userDropdownContentWrapper");
|
||||
await expect(dropdownContentWrapper).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Team" }).click();
|
||||
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
const deleteMemberButton = lastMemberInfo.locator("#deleteMemberButton");
|
||||
await expect(deleteMemberButton).toBeVisible();
|
||||
|
||||
await deleteMemberButton.click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Delete", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
});
|
||||
});
|
||||
@@ -51,3 +51,23 @@ export const replaceEnvironmentIdInHtml = (filePath: string, environmentId: stri
|
||||
writeFileSync(filePath, htmlContent);
|
||||
return "file:///" + filePath;
|
||||
};
|
||||
|
||||
export const signupUsingInviteToken = async (page: Page, name: string, email: string, password: string) => {
|
||||
await page.getByRole("button", { name: "Continue with Email" }).click();
|
||||
await page.getByPlaceholder("Full Name").fill(name);
|
||||
await page.getByPlaceholder("Full Name").press("Tab");
|
||||
|
||||
// the email is already filled in the input field
|
||||
const inputValue = await page.getByPlaceholder("work@email.com").inputValue();
|
||||
expect(inputValue).toEqual(email);
|
||||
|
||||
await page.getByPlaceholder("work@email.com").press("Tab");
|
||||
await page.getByPlaceholder("*******").fill(password);
|
||||
await page.press('input[name="password"]', "Enter");
|
||||
await page.getByRole("link", { name: "Login" }).click();
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
await page.getByPlaceholder("work@email.com").fill(email);
|
||||
await page.getByPlaceholder("*******").click();
|
||||
await page.getByPlaceholder("*******").fill(password);
|
||||
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||
};
|
||||
|
||||
@@ -32,6 +32,18 @@ export const users = {
|
||||
password: "XpP%X9UU3efj8vJa",
|
||||
},
|
||||
],
|
||||
team: [
|
||||
{
|
||||
name: "Team User 1",
|
||||
email: "team1@formbricks.com",
|
||||
password: "Test#1234",
|
||||
},
|
||||
{
|
||||
name: "Team User 2",
|
||||
email: "team2@formbricks.com",
|
||||
password: "Test#1234",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const teams = {
|
||||
@@ -97,3 +109,10 @@ export const surveys = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const invites = {
|
||||
addMember: {
|
||||
name: "Team User 2",
|
||||
email: "team2@formbricks.com",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user