diff --git a/site/e2e/api.ts b/site/e2e/api.ts new file mode 100644 index 0000000000000..88f8666475507 --- /dev/null +++ b/site/e2e/api.ts @@ -0,0 +1,45 @@ +import type { Page } from "@playwright/test"; +import * as API from "api/api"; +import { coderPort } from "./constants"; +import { findSessionToken, randomName } from "./helpers"; + +let currentOrgId: string; + +export const setupApiCalls = async (page: Page) => { + const token = await findSessionToken(page); + API.setSessionToken(token); + API.setHost(`http://127.0.0.1:${coderPort}`); +}; + +export const getCurrentOrgId = async (): Promise => { + if (currentOrgId) { + return currentOrgId; + } + const currentUser = await API.getAuthenticatedUser(); + currentOrgId = currentUser.organization_ids[0]; + return currentOrgId; +}; + +export const createUser = async (orgId: string) => { + const name = randomName(); + const user = await API.createUser({ + email: `${name}@coder.com`, + username: name, + password: "s3cure&password!", + login_type: "password", + disable_login: false, + organization_id: orgId, + }); + return user; +}; + +export const createGroup = async (orgId: string) => { + const name = randomName(); + const group = await API.createGroup(orgId, { + name, + display_name: `Display ${name}`, + avatar_url: "/emojis/1f60d.png", + quota_allowance: 0, + }); + return group; +}; diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 84b1b911c975d..a1fb47816f236 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -7,7 +7,6 @@ import capitalize from "lodash/capitalize"; import path from "path"; import * as ssh from "ssh2"; import { Duplex } from "stream"; -import * as API from "api/api"; import type { WorkspaceBuildParameter, UpdateTemplateMeta, @@ -826,9 +825,3 @@ export async function openTerminalWindow( return terminal; } - -export const setupApiCalls = async (page: Page) => { - const token = await findSessionToken(page); - API.setSessionToken(token); - API.setHost(`http://127.0.0.1:${coderPort}`); -}; diff --git a/site/e2e/tests/groups/addMembers.spec.ts b/site/e2e/tests/groups/addMembers.spec.ts new file mode 100644 index 0000000000000..f9532733d86dd --- /dev/null +++ b/site/e2e/tests/groups/addMembers.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { + createGroup, + createUser, + getCurrentOrgId, + setupApiCalls, +} from "../../api"; +import { requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +test("add members", async ({ page, baseURL }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + const orgId = await getCurrentOrgId(); + const group = await createGroup(orgId); + const numberOfMembers = 3; + const users = await Promise.all( + Array.from({ length: numberOfMembers }, () => createUser(orgId)), + ); + + await page.goto(`${baseURL}/groups/${group.id}`, { + waitUntil: "domcontentloaded", + }); + await expect(page).toHaveTitle(`${group.display_name} - Coder`); + + for (const user of users) { + await page.getByPlaceholder("User email or username").fill(user.username); + await page.getByRole("option", { name: user.email }).click(); + await page.getByRole("button", { name: "Add user" }).click(); + await expect(page.getByRole("row", { name: user.username })).toBeVisible(); + } +}); diff --git a/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts new file mode 100644 index 0000000000000..b5767026c037c --- /dev/null +++ b/site/e2e/tests/groups/addUsersToDefaultGroup.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from "@playwright/test"; +import { createUser, getCurrentOrgId, setupApiCalls } from "../../api"; +import { requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +const DEFAULT_GROUP_NAME = "Everyone"; + +test(`Every user should be automatically added to the default '${DEFAULT_GROUP_NAME}' group upon creation`, async ({ + page, + baseURL, +}) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + const orgId = await getCurrentOrgId(); + const numberOfMembers = 3; + const users = await Promise.all( + Array.from({ length: numberOfMembers }, () => createUser(orgId)), + ); + + await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("Groups - Coder"); + + const groupRow = page.getByRole("row", { name: DEFAULT_GROUP_NAME }); + await groupRow.click(); + await expect(page).toHaveTitle(`${DEFAULT_GROUP_NAME} - Coder`); + + for (const user of users) { + await expect(page.getByRole("row", { name: user.username })).toBeVisible(); + } +}); diff --git a/site/e2e/tests/groups/createGroup.spec.ts b/site/e2e/tests/groups/createGroup.spec.ts new file mode 100644 index 0000000000000..9542f4ea135d2 --- /dev/null +++ b/site/e2e/tests/groups/createGroup.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "@playwright/test"; +import { randomName, requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +test("create group", async ({ page, baseURL }) => { + requiresEnterpriseLicense(); + await page.goto(`${baseURL}/groups`, { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("Groups - Coder"); + + await page.getByText("Create group").click(); + await expect(page).toHaveTitle("Create Group - Coder"); + + const name = randomName(); + const groupValues = { + name: name, + displayName: `Display Name for ${name}`, + avatarURL: "/emojis/1f60d.png", + }; + + await page.getByLabel("Name", { exact: true }).fill(groupValues.name); + await page.getByLabel("Display Name").fill(groupValues.displayName); + await page.getByLabel("Avatar URL").fill(groupValues.avatarURL); + await page.getByRole("button", { name: "Submit" }).click(); + + await expect(page).toHaveTitle(`${groupValues.displayName} - Coder`); + await expect(page.getByText(groupValues.displayName)).toBeVisible(); + await expect(page.getByText("No members yet")).toBeVisible(); +}); diff --git a/site/e2e/tests/groups/navigateToGroupPage.spec.ts b/site/e2e/tests/groups/navigateToGroupPage.spec.ts new file mode 100644 index 0000000000000..44e2224df7c72 --- /dev/null +++ b/site/e2e/tests/groups/navigateToGroupPage.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "@playwright/test"; +import { createGroup, getCurrentOrgId, setupApiCalls } from "../../api"; +import { requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +test("navigate to group page", async ({ page, baseURL }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + const orgId = await getCurrentOrgId(); + const group = await createGroup(orgId); + + await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("Users - Coder"); + + await page.getByRole("link", { name: "Groups" }).click(); + await expect(page).toHaveTitle("Groups - Coder"); + + const groupRow = page.getByRole("row", { name: group.display_name }); + await groupRow.click(); + await expect(page).toHaveTitle(`${group.display_name} - Coder`); +}); diff --git a/site/e2e/tests/groups/removeGroup.spec.ts b/site/e2e/tests/groups/removeGroup.spec.ts new file mode 100644 index 0000000000000..9011ecbb7147a --- /dev/null +++ b/site/e2e/tests/groups/removeGroup.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@playwright/test"; +import { createGroup, getCurrentOrgId, setupApiCalls } from "../../api"; +import { requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +test("remove group", async ({ page, baseURL }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + const orgId = await getCurrentOrgId(); + const group = await createGroup(orgId); + + await page.goto(`${baseURL}/groups/${group.id}`, { + waitUntil: "domcontentloaded", + }); + await expect(page).toHaveTitle(`${group.display_name} - Coder`); + + await page.getByRole("button", { name: "Delete" }).click(); + const dialog = page.getByTestId("dialog"); + await dialog.getByLabel("Name of the group to delete").fill(group.name); + await dialog.getByRole("button", { name: "Delete" }).click(); + await expect(page.getByText("Group deleted successfully.")).toBeVisible(); + + await expect(page).toHaveTitle("Groups - Coder"); +}); diff --git a/site/e2e/tests/groups/removeMember.spec.ts b/site/e2e/tests/groups/removeMember.spec.ts new file mode 100644 index 0000000000000..716c86af84a8d --- /dev/null +++ b/site/e2e/tests/groups/removeMember.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from "@playwright/test"; +import * as API from "api/api"; +import { + createGroup, + createUser, + getCurrentOrgId, + setupApiCalls, +} from "../../api"; +import { requiresEnterpriseLicense } from "../../helpers"; +import { beforeCoderTest } from "../../hooks"; + +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); + +test("remove member", async ({ page, baseURL }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + const orgId = await getCurrentOrgId(); + const [group, member] = await Promise.all([ + createGroup(orgId), + createUser(orgId), + ]); + await API.addMember(group.id, member.id); + + await page.goto(`${baseURL}/groups/${group.id}`, { + waitUntil: "domcontentloaded", + }); + await expect(page).toHaveTitle(`${group.display_name} - Coder`); + + const userRow = page.getByRole("row", { name: member.username }); + await userRow.getByRole("button", { name: "More options" }).click(); + + const menu = page.locator("#more-options"); + await menu.getByText("Remove").click({ timeout: 1_000 }); + + await expect(page.getByText("Member removed successfully.")).toBeVisible(); +}); diff --git a/site/e2e/tests/users/removeUser.spec.ts b/site/e2e/tests/users/removeUser.spec.ts index c6e60c25e604d..cd09d13611e60 100644 --- a/site/e2e/tests/users/removeUser.spec.ts +++ b/site/e2e/tests/users/removeUser.spec.ts @@ -1,29 +1,21 @@ import { test, expect } from "@playwright/test"; -import * as API from "api/api"; -import { randomName, setupApiCalls } from "../../helpers"; +import { createUser, getCurrentOrgId, setupApiCalls } from "../../api"; import { beforeCoderTest } from "../../hooks"; test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("remove user", async ({ page, baseURL }) => { await setupApiCalls(page); - const currentUser = await API.getAuthenticatedUser(); - const name = randomName(); - const user = await API.createUser({ - email: `${name}@coder.com`, - username: name, - password: "s3cure&password!", - login_type: "password", - disable_login: false, - organization_id: currentUser.organization_ids[0], - }); + const orgId = await getCurrentOrgId(); + const user = await createUser(orgId); await page.goto(`${baseURL}/users`, { waitUntil: "domcontentloaded" }); await expect(page).toHaveTitle("Users - Coder"); - const userRow = page.locator("tr", { hasText: user.email }); + const userRow = page.getByRole("row", { name: user.email }); await userRow.getByRole("button", { name: "More options" }).click(); - await userRow.getByText("Delete", { exact: false }).click(); + const menu = page.locator("#more-options"); + await menu.getByText("Delete").click(); const dialog = page.getByTestId("dialog"); await dialog.getByLabel("Name of the user to delete").fill(user.username); diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 760f860ebe3c1..12c2a63b2c014 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1147,7 +1147,6 @@ export const patchGroup = async ( export const addMember = async (groupId: string, userId: string) => { return patchGroup(groupId, { name: "", - display_name: "", add_users: [userId], remove_users: [], }); diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index f1f3a7bd24fc9..01e8dc250b13b 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -197,6 +197,7 @@ export const GroupPage: FC = () => { onConfirm={async () => { try { await deleteGroupMutation.mutateAsync(groupId); + displaySuccess("Group deleted successfully."); navigate("/groups"); } catch (error) { displayError(getErrorMessage(error, "Failed to delete group.")); diff --git a/site/src/pages/UsersPage/UsersLayout.tsx b/site/src/pages/UsersPage/UsersLayout.tsx index dc39ae33acc23..bb85cae1b03b8 100644 --- a/site/src/pages/UsersPage/UsersLayout.tsx +++ b/site/src/pages/UsersPage/UsersLayout.tsx @@ -1,7 +1,6 @@ import GroupAdd from "@mui/icons-material/GroupAddOutlined"; import PersonAdd from "@mui/icons-material/PersonAddOutlined"; import Button from "@mui/material/Button"; -import Link from "@mui/material/Link"; import { type FC, Suspense } from "react"; import { Link as RouterLink, @@ -43,9 +42,13 @@ export const UsersLayout: FC = () => { )} {canCreateGroup && isTemplateRBACEnabled && ( - - - + )} }