Files
authentik/web/test/browser/groups.test.ts
Teffen Ellis b6496950bf web: Close modal on route navigation (#21622)
* Close dialog on navigation.

* web: update dialog, form, and sidebar styles with logical properties and scroll shadows

Migrate dialog padding CSS variables from physical (top/right/bottom/left) to logical
(block-start/inline-end/block-end/inline-start) naming. Add scroll shadow utility
class (ak-m-scroll-shadows) for scrollable regions. Rework radio input and form
control styles including transparent backgrounds, checkbox-style indicators, and
improved hover states. Refactor FormGroup marker to use CSS custom properties for
open/closed states. Move sidebar padding from nav container to scrollable list.

* web: refine elements and components for accessibility, type safety, and consistency

Add ARIA role and label to dialog body, apply scroll shadow classes to modal body,
sidebar nav, and wizard main. Update ak-status-label to support tri-state
(good/bad/null) rendering with ts-pattern matching and a neutral label. Simplify
FormGroup by removing wrapper div around default slot, adding part attributes for
header styling, and changing description to nullable type. Clean up LogViewer and
StaticTable with proper access modifiers, override annotations, and nullable item
types. Simplify ak-switch-input checked binding and remove unused slot attribute
from ak-radio-input help text.

* web: modernize application pages with modalInvoker and updated form patterns

Refactor ApplicationCheckAccessForm to use static form metadata properties
(verboseName, submitVerb, createLabel), formatAPISuccessMessage, and a private
CoreApi instance. Migrate ApplicationViewPage from ak-forms-modal slots to the
modalInvoker directive for both edit and check-access actions. Accept nullable
input in createPaginatedResponse for better null-safety. Fix casing of dropdown
menu items in ApplicationListPage.

* web: migrate remaining view pages to modalInvoker (#21592)

* Fix visibility check, search params.

* Add scroll shadow.

* Partial revert of input layout.

* Tidy groups.

* Fix check access form invoker, styles.

* Optional sizing.

* Lowercase

* Revise checkbox style.

* Close dialog on navigation.

* Fix padding.

* Touch up shadow heights.

* Migrate remaining view pages to modalInvoker, add e2e coverage.

* Fix alignment.

* Fix click handler, add placeholders.

* Fix issue where form field is not serialized.
2026-04-16 19:04:29 +02:00

302 lines
10 KiB
TypeScript

import { expect, test } from "#e2e";
import { randomName } from "#e2e/utils/generators";
import { IDGenerator } from "@goauthentik/core/id";
import { series } from "@goauthentik/core/promises";
import { snakeCase } from "change-case";
test.describe("Groups", () => {
const adminGroupName = "authentik Admins";
const adminUsername = "akadmin";
const usernames = new Map<string, string>();
const groupNames = new Map<string, string>();
//#region Lifecycle
test.beforeEach("Prepare user", async ({ session }, { testId }) => {
const seed = IDGenerator.randomID(6);
const groupName = `${randomName(seed)} (${seed})`;
groupNames.set(testId, groupName);
usernames.set(testId, snakeCase(groupName));
await test.step("Authenticate", async () => {
await session.login({
to: "/if/admin/#/identity/groups",
});
});
});
//#endregion
//#region Tests
test("Creating a user within the admin group", async ({
navigator,
form,
pointer,
page,
}, testInfo) => {
const { fill, search } = form;
const { click } = pointer;
const displayName = groupNames.get(testInfo.testId)!;
const username = usernames.get(testInfo.testId)!;
const adminsURL = await test.step("Find admin group via search", async () => {
const $adminGroupRow = await search(adminGroupName);
await expect($adminGroupRow, "Admin group is visible").toBeVisible();
const groupLink = $adminGroupRow.getByRole("link", { name: "view details" });
await expect(groupLink, "Admin group link is visible").toBeVisible();
return groupLink.evaluate((el: HTMLAnchorElement) => el.href);
});
expect(adminsURL, "Admin group link has href").not.toBeNull();
await navigator.navigate(adminsURL);
await test.step("User creation", async () => {
await click("Users", "tab");
const dialog = page.getByRole("dialog", { name: "New Group User" });
await expect(dialog, "Dialog is initially closed").toBeHidden();
await click("Add New User", "button");
await click("New Group User...", "menuitem");
await expect(dialog, "Dialog opens").toBeVisible();
await series(
[fill, /^Username/, username, dialog],
[fill, /^Display Name/, displayName, dialog],
[fill, /^Email Address/, `${username}@example.com`, dialog],
);
await dialog.getByRole("button", { name: "Create User" }).click();
await dialog.waitFor({ state: "hidden" });
await expect(dialog, "Dialog closes after creating user").toBeHidden();
});
await test.step("Verify user creation", async () => {
const $user = await test.step("Find user via search", () => {
const context = page.getByRole("tabpanel", { name: "Users" });
return search(username, context);
});
await expect($user, "User is visible").toBeVisible();
});
});
test("Simple group", async ({ form, pointer, page }, testInfo) => {
const groupName = groupNames.get(testInfo.testId)!;
const { fill, search } = form;
const { click } = pointer;
const dialog = page.getByRole("dialog", { name: "New Group" });
await test.step("Group Creation", async () => {
await expect(dialog, "Dialog is initially closed").toBeHidden();
await click("New Group", "button");
await expect(dialog, "Dialog opens").toBeVisible();
await series(
// ---
[fill, /^Group Name/, groupName, dialog],
);
const createButton = dialog.getByRole("button", { name: "Create Group" });
await expect(createButton, "Create button is visible").toBeVisible();
await createButton.click();
await expect(dialog, "Dialog closes after creating group").toBeHidden({
timeout: 10_000,
});
});
await test.step("Verify group creation", async () => {
const groupRow = await test.step("Find group via search", () => search(groupName));
await expect(groupRow, "Group is visible").toBeVisible();
await groupRow.getByRole("link", { name: "view details" }).click();
});
await test.step("Assigning a user to the group", async () => {
const assignUsersModal = page.getByRole("dialog", { name: "Assign Additional Users" });
const selectUsersModal = page.getByRole("dialog", { name: "Select users" });
await series(
// ---
[click, "users", "tab"],
[click, "Add existing user", "button"],
[click, "Open user selection dialog", "button"],
);
const adminRow = await test.step("Find admin via search", () =>
search(adminUsername, selectUsersModal));
await expect(adminRow, "Admin is visible").toBeVisible();
await adminRow.getByRole("checkbox").check();
const confirmButton = selectUsersModal.getByRole("button", { name: "Confirm" });
await expect(confirmButton, "Confirm button is visible").toBeVisible();
await confirmButton.click();
const assignButton = assignUsersModal.getByRole("button", { name: "Assign" });
await expect(assignButton, "Assign button is visible").toBeVisible();
await assignButton.click();
await expect(assignUsersModal, "Assign users modal closes").toBeHidden({
timeout: 10_000,
});
await test.step("Verify admin user assignment", async () => {
// eslint-disable-next-line max-nested-callbacks
const groupRow = await test.step("Find group via search", () => {
const context = page.getByRole("tabpanel", { name: "Users" });
return search(adminUsername, context);
});
await expect(groupRow, "Group is visible").toBeVisible();
});
});
});
test("Edit group from view page", async ({ navigator, form, pointer, page }, testInfo) => {
const groupName = groupNames.get(testInfo.testId)!;
const { fill, search } = form;
const { click } = pointer;
const newGroupDialog = page.getByRole("dialog", { name: "New Group" });
const editGroupDialog = page.getByRole("dialog", { name: "Edit Group" });
await test.step("Create group", async () => {
await click("New Group", "button");
await expect(newGroupDialog, "Dialog opens").toBeVisible();
await fill(/^Group Name/, groupName, newGroupDialog);
await newGroupDialog.getByRole("button", { name: "Create Group" }).click();
await expect(newGroupDialog, "Dialog closes after creating group").toBeHidden({
timeout: 10_000,
});
});
await test.step("Navigate to group view page", async () => {
const $group = await search(groupName);
await expect($group, "Group is visible").toBeVisible();
const viewLink = $group.getByRole("link", { name: "view details" });
await expect(viewLink, "View details link is visible").toBeVisible();
await viewLink.click();
});
const updatedName = `${groupName} Edited`;
await test.step("Edit group from view page", async () => {
await expect(editGroupDialog, "Edit dialog is initially closed").toBeHidden();
await click("Edit", "button");
await expect(editGroupDialog, "Edit dialog opens").toBeVisible();
const nameInput = editGroupDialog.getByRole("textbox", { name: /Group Name/ });
await expect(nameInput, "Name input is visible").toBeVisible();
await expect(nameInput, "Name is pre-filled").toHaveValue(groupName);
await nameInput.fill(updatedName);
await editGroupDialog.getByRole("button", { name: "Save Changes" }).click();
await expect(editGroupDialog, "Edit dialog closes after saving").toBeHidden();
});
await test.step("Verify group name updated on view page", async () => {
await expect(
page.getByRole("heading", { name: updatedName }).first(),
"Updated group name is visible on view page",
).toBeVisible();
});
});
test("Edit group from related group list", async ({
navigator,
form,
pointer,
page,
}, testInfo) => {
const groupName = groupNames.get(testInfo.testId)!;
const { fill, search } = form;
const { click } = pointer;
const newGroupDialog = page.getByRole("dialog", { name: "New Group" });
await test.step("Create group with admin user", async () => {
await click("New Group", "button");
await expect(newGroupDialog, "Dialog opens").toBeVisible();
await fill(/^Group Name/, groupName, newGroupDialog);
await newGroupDialog.getByRole("button", { name: "Create Group" }).click();
await expect(newGroupDialog, "Dialog closes").toBeHidden({ timeout: 10_000 });
});
await test.step("Navigate to admin user", async () => {
await navigator.navigate("/if/admin/#/identity/users");
const $adminUser = await search(adminUsername);
await expect($adminUser, "Admin user is visible").toBeVisible();
const viewLink = $adminUser.getByRole("link", {
name: "View details for authentik Default Admin",
});
await expect(viewLink, "View details link is visible").toBeVisible();
await viewLink.click();
});
await test.step("Add user to group via related group list", async () => {
await click("Groups", "tab");
const groupsPanel = page.getByRole("tabpanel", { name: "Groups" });
const addGroupDialog = page.getByRole("dialog", { name: "Add Group" });
await expect(addGroupDialog, "Add dialog is initially closed").toBeHidden();
await groupsPanel.getByRole("button", { name: "Add to existing group" }).click();
await expect(addGroupDialog, "Add dialog opens").toBeVisible();
});
});
//#endregion
});