Skip to content

Commit 250b2c0

Browse files
committed
Add team invite test
1 parent adcc97d commit 250b2c0

File tree

5 files changed

+141
-12
lines changed

5 files changed

+141
-12
lines changed

app/(dashboard)/dashboard/page.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { Loader2, PlusCircle } from "lucide-react";
4-
import { Suspense, useActionState } from "react";
4+
import { Suspense, useActionState, useEffect } from "react";
55
import useSWR from "swr";
66
import { inviteTeamMember, removeTeamMember } from "@/app/(login)/actions";
77
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -21,6 +21,7 @@ import type { TeamDataWithMembers, User } from "@/lib/db/schema";
2121
type ActionState = {
2222
error?: string;
2323
success?: string;
24+
id?: number;
2425
};
2526

2627
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -123,7 +124,11 @@ function TeamMembers() {
123124
<CardContent>
124125
<ul className="space-y-4">
125126
{teamData.teamMembers.map((member, index) => (
126-
<li key={member.id} className="flex items-center justify-between">
127+
<li
128+
data-testid="team-member"
129+
key={member.id}
130+
className="flex items-center justify-between"
131+
>
127132
<div className="flex items-center space-x-4">
128133
<Avatar>
129134
{/*
@@ -143,10 +148,13 @@ function TeamMembers() {
143148
</AvatarFallback>
144149
</Avatar>
145150
<div>
146-
<p className="font-medium">
151+
<p data-testid="team-member-name" className="font-medium">
147152
{getUserDisplayName(member.user)}
148153
</p>
149-
<p className="text-sm text-muted-foreground capitalize">
154+
<p
155+
data-testid="team-member-role"
156+
className="text-sm text-muted-foreground capitalize"
157+
>
150158
{member.role}
151159
</p>
152160
</div>
@@ -193,6 +201,12 @@ function InviteTeamMember() {
193201
FormData
194202
>(inviteTeamMember, {});
195203

204+
useEffect(() => {
205+
if (inviteState?.id) {
206+
console.log("[inviteState]", inviteState.id);
207+
}
208+
}, [inviteState]);
209+
196210
return (
197211
<Card>
198212
<CardHeader>

app/(login)/actions.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,16 @@ export const inviteTeamMember = validatedActionWithUser(
330330
}
331331

332332
// Create a new invitation
333-
await db.insert(invitations).values({
334-
teamId: userWithTeam.teamId,
335-
email,
336-
role,
337-
invitedBy: user.id,
338-
status: "pending",
339-
});
333+
const invitation = await db
334+
.insert(invitations)
335+
.values({
336+
teamId: userWithTeam.teamId,
337+
email,
338+
role,
339+
invitedBy: user.id,
340+
status: "pending",
341+
})
342+
.returning();
340343

341344
await logActivity(
342345
userWithTeam.teamId,
@@ -347,6 +350,6 @@ export const inviteTeamMember = validatedActionWithUser(
347350
// TODO: Send invitation email and include ?inviteId={id} to sign-up URL
348351
// await sendInvitationEmail(email, userWithTeam.team.name, role)
349352

350-
return { success: "Invitation sent successfully" };
353+
return { success: "Invitation sent successfully", id: invitation[0].id };
351354
},
352355
);

tests/team-invitation.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test.describe("Team Invitation Flow", () => {
4+
test("should successfully invite a team member and complete signup process", async ({
5+
page,
6+
context,
7+
}) => {
8+
const newUserEmail = `testuser${Date.now()}@example.com`;
9+
let invitationId: string | null = null;
10+
11+
const existingMemberName =
12+
await test.step("Navigate to dashboard and verify current team member", async () => {
13+
await page.goto("/dashboard");
14+
await expect(
15+
page.getByRole("heading", { name: "Team Settings" }),
16+
).toBeVisible();
17+
18+
const existingMember = await page.getByTestId("team-member");
19+
const existingMemberName = await existingMember
20+
.getByTestId("team-member-name")
21+
.textContent();
22+
if (!existingMemberName) {
23+
throw new Error("Existing member name not found");
24+
}
25+
expect(
26+
await existingMember.getByTestId("team-member-role"),
27+
).toContainText("owner");
28+
29+
return existingMemberName;
30+
});
31+
32+
await test.step("Send team invitation", async () => {
33+
page.on("console", (msg) => {
34+
const text = msg.text();
35+
const match = text.match(/\[inviteState\]\s+(\d+)/);
36+
if (match) {
37+
invitationId = match[1];
38+
console.log(`Captured invitation ID: ${invitationId}`);
39+
}
40+
});
41+
42+
await page.getByRole("textbox", { name: "Email" }).fill(newUserEmail);
43+
await expect(page.getByRole("radio", { name: "Member" })).toBeChecked();
44+
await page.getByRole("button", { name: "Invite Member" }).click();
45+
await expect(
46+
page.getByText("Invitation sent successfully"),
47+
).toBeVisible();
48+
49+
await page.waitForTimeout(1000);
50+
expect(invitationId).not.toBeNull();
51+
});
52+
53+
await test.step("Sign out current user", async () => {
54+
await page.getByTestId("user-menu-trigger").click();
55+
await page.getByRole("button", { name: "Sign out" }).click();
56+
});
57+
58+
await test.step("Complete signup process with invitation", async () => {
59+
await page.goto("/sign-up?inviteId=" + invitationId);
60+
await expect(
61+
page.getByRole("heading", { name: "Create your account" }),
62+
).toBeVisible();
63+
64+
await page.getByRole("textbox", { name: "Email" }).fill(newUserEmail);
65+
await page
66+
.getByRole("textbox", { name: "Password" })
67+
.fill("testpassword123");
68+
await page.getByRole("button", { name: "Sign up" }).click();
69+
});
70+
71+
await test.step("Verify successful team member addition", async () => {
72+
await expect(
73+
page.getByRole("heading", { name: "Team Settings" }),
74+
).toBeVisible();
75+
await expect(page).toHaveURL("/dashboard");
76+
77+
const firstTeamMember = await page.getByTestId("team-member").first();
78+
expect(firstTeamMember.getByTestId("team-member-name")).toContainText(
79+
existingMemberName,
80+
);
81+
expect(firstTeamMember.getByTestId("team-member-role")).toContainText(
82+
"owner",
83+
);
84+
85+
const secondTeamMember = await page.getByTestId("team-member").nth(1);
86+
expect(secondTeamMember.getByTestId("team-member-name")).toContainText(
87+
newUserEmail,
88+
);
89+
expect(secondTeamMember.getByTestId("team-member-role")).toContainText(
90+
"member",
91+
);
92+
});
93+
});
94+
});

tutorial.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const tutorialConfig: TutorialConfig = {
3737
"tests/change-name.spec.ts",
3838
"tests/plan-upgrade.spec.ts",
3939
"tests/change-email.spec.ts",
40+
"tests/team-invitation.spec.ts",
4041
],
4142
},
4243
{

tutorial/stage-2-generated-tests/generate-tests-playwright-mcp.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,23 @@ Here are a few more prompts you can try out to generate more tests (don't forget
389389
- verify that the new email is displayed on the general page in the email field.
390390
```
391391

392+
#### Invite a new user
393+
```
394+
# Test scenario
395+
- Navigate to the dashboard at /dashboard.
396+
- Navigate to the team settings section.
397+
- Copy the name/email of the currently only team member.
398+
- Fill in the Invite Team Member email field with a new randomly generated email.
399+
- Click the "Invite" button.
400+
- The application will console.log the invitation id in the format [inviteState] {id}.
401+
r
402+
- Sign out
403+
- Sign up with the new email and password "testpassword123".
404+
- Verify that we are logged in and on the team settings page.
405+
- Verify that there are two team members in the team members list.
406+
- Verify that the new team member's name/email is displayed in the team members list.
407+
```
408+
392409

393410

394411
## List of bugs we found and fixed in the course of generating these tests

0 commit comments

Comments
 (0)