Skip to content

Commit 06e7739

Browse files
authored
chore: add e2e tests for organization members (coder#15807)
1 parent 29c9bbf commit 06e7739

File tree

11 files changed

+154
-107
lines changed

11 files changed

+154
-107
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8621,6 +8621,10 @@ func (q *FakeQuerier) OrganizationMembers(_ context.Context, arg database.Organi
86218621
tmp = append(tmp, database.OrganizationMembersRow{
86228622
OrganizationMember: organizationMember,
86238623
Username: user.Username,
8624+
AvatarURL: user.AvatarURL,
8625+
Name: user.Name,
8626+
Email: user.Email,
8627+
GlobalRoles: user.RBACRoles,
86248628
})
86258629
}
86268630
return tmp, nil

site/e2e/helpers.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type ChildProcess, exec, spawn } from "node:child_process";
22
import { randomUUID } from "node:crypto";
3+
import * as fs from "node:fs";
34
import net from "node:net";
45
import path from "node:path";
56
import { Duplex } from "node:stream";
@@ -850,6 +851,7 @@ export const fillParameters = async (
850851

851852
export const updateTemplate = async (
852853
page: Page,
854+
organization: string,
853855
templateName: string,
854856
responses?: EchoProvisionerResponses,
855857
) => {
@@ -868,6 +870,8 @@ export const updateTemplate = async (
868870
"-y",
869871
"-d",
870872
"-",
873+
"-O",
874+
organization,
871875
templateName,
872876
],
873877
{
@@ -880,6 +884,7 @@ export const updateTemplate = async (
880884
);
881885

882886
const uploaded = new Awaiter();
887+
883888
child.on("exit", (code) => {
884889
if (code === 0) {
885890
uploaded.done();
@@ -987,3 +992,48 @@ export async function openTerminalWindow(
987992

988993
return terminal;
989994
}
995+
996+
type UserValues = {
997+
name: string;
998+
username: string;
999+
email: string;
1000+
password: string;
1001+
};
1002+
1003+
export async function createUser(
1004+
page: Page,
1005+
userValues: Partial<UserValues> = {},
1006+
): Promise<UserValues> {
1007+
const returnTo = page.url();
1008+
1009+
await page.goto("/deployment/users", { waitUntil: "domcontentloaded" });
1010+
await expect(page).toHaveTitle("Users - Coder");
1011+
1012+
await page.getByRole("button", { name: "Create user" }).click();
1013+
await expect(page).toHaveTitle("Create User - Coder");
1014+
1015+
const username = userValues.username ?? randomName();
1016+
const name = userValues.name ?? username;
1017+
const email = userValues.email ?? `${username}@coder.com`;
1018+
const password = userValues.password || "s3cure&password!";
1019+
1020+
await page.getByLabel("Username").fill(username);
1021+
if (name) {
1022+
await page.getByLabel("Full name").fill(name);
1023+
}
1024+
await page.getByLabel("Email").fill(email);
1025+
await page.getByLabel("Login Type").click();
1026+
await page.getByRole("option", { name: "Password", exact: false }).click();
1027+
// Using input[name=password] due to the select element utilizing 'password'
1028+
// as the label for the currently active option.
1029+
const passwordField = page.locator("input[name=password]");
1030+
await passwordField.fill(password);
1031+
await page.getByRole("button", { name: "Create user" }).click();
1032+
await expect(page.getByText("Successfully created user.")).toBeVisible();
1033+
1034+
await expect(page).toHaveTitle("Users - Coder");
1035+
await expect(page.locator("tr", { hasText: email })).toBeVisible();
1036+
1037+
await page.goto(returnTo, { waitUntil: "domcontentloaded" });
1038+
return { name, username, email, password };
1039+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect, test } from "@playwright/test";
2+
import { setupApiCalls } from "../api";
3+
import { expectUrl } from "../expectUrl";
4+
import { createUser, randomName, requiresLicense } from "../helpers";
5+
import { beforeCoderTest } from "../hooks";
6+
7+
test.beforeEach(async ({ page }) => {
8+
await beforeCoderTest(page);
9+
await setupApiCalls(page);
10+
});
11+
12+
test("add and remove organization member", async ({ page }) => {
13+
requiresLicense();
14+
15+
// Create a new organization to test
16+
await page.goto("/organizations/new", { waitUntil: "domcontentloaded" });
17+
const name = randomName();
18+
await page.getByLabel("Slug").fill(name);
19+
await page.getByLabel("Display name").fill(`Org ${name}`);
20+
await page.getByLabel("Description").fill(`Org description ${name}`);
21+
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
22+
await page.getByRole("button", { name: "Submit" }).click();
23+
24+
// Navigate to members page
25+
await expectUrl(page).toHavePathName(`/organizations/${name}`);
26+
await expect(page.getByText("Organization created.")).toBeVisible();
27+
await page.getByText("Members").click();
28+
29+
// Add a user to the org
30+
const personToAdd = await createUser(page);
31+
await page.getByPlaceholder("User email or username").fill(personToAdd.email);
32+
await page.getByRole("option", { name: personToAdd.email }).click();
33+
await page.getByRole("button", { name: "Add user" }).click();
34+
const addedRow = page.locator("tr", { hasText: personToAdd.email });
35+
await expect(addedRow).toBeVisible();
36+
37+
// Give them a role
38+
await addedRow.getByLabel("Edit user roles").click();
39+
await page.getByText("Organization User Admin").click();
40+
await page.getByText("Organization Template Admin").click();
41+
await page.mouse.click(10, 10); // close the popover by clicking outside of it
42+
await expect(addedRow.getByText("Organization User Admin")).toBeVisible();
43+
await expect(addedRow.getByText("+1 more")).toBeVisible();
44+
45+
// Remove them from the org
46+
await addedRow.getByLabel("More options").click();
47+
await page.getByText("Remove").click(); // Click the "Remove" option
48+
await page.getByRole("button", { name: "Remove" }).click(); // Click "Remove" in the confirmation dialog
49+
await expect(addedRow).not.toBeVisible();
50+
});

site/e2e/tests/organizations.spec.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, test } from "@playwright/test";
22
import { setupApiCalls } from "../api";
33
import { expectUrl } from "../expectUrl";
4-
import { requiresLicense } from "../helpers";
4+
import { randomName, requiresLicense } from "../helpers";
55
import { beforeCoderTest } from "../hooks";
66

77
test.beforeEach(async ({ page }) => {
@@ -17,27 +17,29 @@ test("create and delete organization", async ({ page }) => {
1717
waitUntil: "domcontentloaded",
1818
});
1919

20-
await page.getByLabel("Slug").fill("floop");
21-
await page.getByLabel("Display name").fill("Floop");
22-
await page.getByLabel("Description").fill("Org description floop");
20+
const name = randomName();
21+
await page.getByLabel("Slug").fill(name);
22+
await page.getByLabel("Display name").fill(`Org ${name}`);
23+
await page.getByLabel("Description").fill(`Org description ${name}`);
2324
await page.getByLabel("Icon", { exact: true }).fill("/emojis/1f957.png");
2425
await page.getByRole("button", { name: "Submit" }).click();
2526

2627
// Expect to be redirected to the new organization
27-
await expectUrl(page).toHavePathName("/organizations/floop");
28+
await expectUrl(page).toHavePathName(`/organizations/${name}`);
2829
await expect(page.getByText("Organization created.")).toBeVisible();
2930

30-
await page.getByLabel("Slug").fill("wibble");
31-
await page.getByLabel("Description").fill("Org description wibble");
31+
const newName = randomName();
32+
await page.getByLabel("Slug").fill(newName);
33+
await page.getByLabel("Description").fill(`Org description ${newName}`);
3234
await page.getByRole("button", { name: "Submit" }).click();
3335

3436
// Expect to be redirected when renaming the organization
35-
await expectUrl(page).toHavePathName("/organizations/wibble");
37+
await expectUrl(page).toHavePathName(`/organizations/${newName}`);
3638
await expect(page.getByText("Organization settings updated.")).toBeVisible();
3739

3840
await page.getByRole("button", { name: "Delete this organization" }).click();
3941
const dialog = page.getByTestId("dialog");
40-
await dialog.getByLabel("Name").fill("wibble");
42+
await dialog.getByLabel("Name").fill(newName);
4143
await dialog.getByRole("button", { name: "Delete" }).click();
4244
await expect(page.getByText("Organization deleted.")).toBeVisible();
4345
});
Lines changed: 6 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,13 @@
1-
import { expect, test } from "@playwright/test";
2-
import { randomName } from "../../helpers";
1+
import { test } from "@playwright/test";
2+
import { createUser } from "../../helpers";
33
import { beforeCoderTest } from "../../hooks";
44

55
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
66

7-
test("create user with password", async ({ page, baseURL }) => {
8-
await page.goto(`${baseURL}/deployment/users`, {
9-
waitUntil: "domcontentloaded",
10-
});
11-
await expect(page).toHaveTitle("Users - Coder");
12-
13-
await page.getByRole("button", { name: "Create user" }).click();
14-
await expect(page).toHaveTitle("Create User - Coder");
15-
16-
const name = randomName();
17-
const userValues = {
18-
username: name,
19-
name: name,
20-
email: `${name}@coder.com`,
21-
loginType: "password",
22-
password: "s3cure&password!",
23-
};
24-
25-
await page.getByLabel("Username").fill(userValues.username);
26-
await page.getByLabel("Full name").fill(userValues.username);
27-
await page.getByLabel("Email").fill(userValues.email);
28-
await page.getByLabel("Login Type").click();
29-
await page.getByRole("option", { name: "Password", exact: false }).click();
30-
// Using input[name=password] due to the select element utilizing 'password'
31-
// as the label for the currently active option.
32-
const passwordField = page.locator("input[name=password]");
33-
await passwordField.fill(userValues.password);
34-
await page.getByRole("button", { name: "Create user" }).click();
35-
await expect(page.getByText("Successfully created user.")).toBeVisible();
36-
37-
await expect(page).toHaveTitle("Users - Coder");
38-
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
7+
test("create user with password", async ({ page }) => {
8+
await createUser(page);
399
});
4010

41-
test("create user without full name is optional", async ({ page, baseURL }) => {
42-
await page.goto(`${baseURL}/deployment/users`, {
43-
waitUntil: "domcontentloaded",
44-
});
45-
await expect(page).toHaveTitle("Users - Coder");
46-
47-
await page.getByRole("button", { name: "Create user" }).click();
48-
await expect(page).toHaveTitle("Create User - Coder");
49-
50-
const name = randomName();
51-
const userValues = {
52-
username: name,
53-
email: `${name}@coder.com`,
54-
loginType: "password",
55-
password: "s3cure&password!",
56-
};
57-
58-
await page.getByLabel("Username").fill(userValues.username);
59-
await page.getByLabel("Email").fill(userValues.email);
60-
await page.getByLabel("Login Type").click();
61-
await page.getByRole("option", { name: "Password", exact: false }).click();
62-
// Using input[name=password] due to the select element utilizing 'password'
63-
// as the label for the currently active option.
64-
const passwordField = page.locator("input[name=password]");
65-
await passwordField.fill(userValues.password);
66-
await page.getByRole("button", { name: "Create user" }).click();
67-
await expect(page.getByText("Successfully created user.")).toBeVisible();
68-
69-
await expect(page).toHaveTitle("Users - Coder");
70-
await expect(page.locator("tr", { hasText: userValues.email })).toBeVisible();
11+
test("create user without full name", async ({ page }) => {
12+
await createUser(page, { name: "" });
7113
});

site/e2e/tests/workspaces/updateWorkspace.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ test("update workspace, new optional, immutable parameter added", async ({
4141
const updatedRichParameters = [...richParameters, fifthParameter];
4242
await updateTemplate(
4343
page,
44+
"coder",
4445
template,
4546
echoResponsesWithParameters(updatedRichParameters),
4647
);
@@ -79,6 +80,7 @@ test("update workspace, new required, mutable parameter added", async ({
7980
const updatedRichParameters = [...richParameters, sixthParameter];
8081
await updateTemplate(
8182
page,
83+
"coder",
8284
template,
8385
echoResponsesWithParameters(updatedRichParameters),
8486
);

site/src/pages/ManagementSettingsPage/OrganizationMembersPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ const OrganizationMembersPage: FC = () => {
7777
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
7878
me={me}
7979
members={members}
80-
groupsByUserId={groupsByUserIdQuery.data}
8180
addMember={async (user: User) => {
8281
await addMemberMutation.mutateAsync(user.id);
8382
void membersQuery.refetch();

site/src/pages/ManagementSettingsPage/OrganizationMembersPageView.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ interface OrganizationMembersPageViewProps {
4242
isUpdatingMemberRoles: boolean;
4343
me: User;
4444
members: Array<OrganizationMemberTableEntry> | undefined;
45-
groupsByUserId: GroupsByUserId | undefined;
4645
addMember: (user: User) => Promise<void>;
4746
removeMember: (member: OrganizationMemberWithUserData) => void;
4847
updateMemberRoles: (
@@ -57,17 +56,28 @@ interface OrganizationMemberTableEntry extends OrganizationMemberWithUserData {
5756

5857
export const OrganizationMembersPageView: FC<
5958
OrganizationMembersPageViewProps
60-
> = (props) => {
59+
> = ({
60+
allAvailableRoles,
61+
canEditMembers,
62+
error,
63+
isAddingMember,
64+
isUpdatingMemberRoles,
65+
me,
66+
members,
67+
addMember,
68+
removeMember,
69+
updateMemberRoles,
70+
}) => {
6171
return (
6272
<div>
6373
<SettingsHeader title="Members" />
6474
<Stack>
65-
{Boolean(props.error) && <ErrorAlert error={props.error} />}
75+
{Boolean(error) && <ErrorAlert error={error} />}
6676

67-
{props.canEditMembers && (
77+
{canEditMembers && (
6878
<AddOrganizationMember
69-
isLoading={props.isAddingMember}
70-
onSubmit={props.addMember}
79+
isLoading={isAddingMember}
80+
onSubmit={addMember}
7181
/>
7282
)}
7383

@@ -92,7 +102,7 @@ export const OrganizationMembersPageView: FC<
92102
</TableRow>
93103
</TableHead>
94104
<TableBody>
95-
{props.members?.map((member) => (
105+
{members?.map((member) => (
96106
<TableRow key={member.user_id}>
97107
<TableCell>
98108
<AvatarData
@@ -109,13 +119,13 @@ export const OrganizationMembersPageView: FC<
109119
<UserRoleCell
110120
inheritedRoles={member.global_roles}
111121
roles={member.roles}
112-
allAvailableRoles={props.allAvailableRoles}
122+
allAvailableRoles={allAvailableRoles}
113123
oidcRoleSyncEnabled={false}
114-
isLoading={props.isUpdatingMemberRoles}
115-
canEditUsers={props.canEditMembers}
124+
isLoading={isUpdatingMemberRoles}
125+
canEditUsers={canEditMembers}
116126
onEditRoles={async (roles) => {
117127
try {
118-
await props.updateMemberRoles(member, roles);
128+
await updateMemberRoles(member, roles);
119129
displaySuccess("Roles updated successfully.");
120130
} catch (error) {
121131
displayError(
@@ -126,15 +136,15 @@ export const OrganizationMembersPageView: FC<
126136
/>
127137
<UserGroupsCell userGroups={member.groups} />
128138
<TableCell>
129-
{member.user_id !== props.me.id && props.canEditMembers && (
139+
{member.user_id !== me.id && canEditMembers && (
130140
<MoreMenu>
131141
<MoreMenuTrigger>
132142
<ThreeDotsButton />
133143
</MoreMenuTrigger>
134144
<MoreMenuContent>
135145
<MoreMenuItem
136146
danger
137-
onClick={() => props.removeMember(member)}
147+
onClick={() => removeMember(member)}
138148
>
139149
Remove
140150
</MoreMenuItem>

site/src/pages/ManagementSettingsPage/UserTable/EditRolesButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
128128
</Tooltip>
129129
</PopoverTrigger>
130130

131-
<PopoverContent classes={{ paper }}>
131+
<PopoverContent classes={{ paper }} disablePortal={false}>
132132
<fieldset
133133
css={styles.fieldset}
134134
disabled={isLoading}

0 commit comments

Comments
 (0)