test: add e2e tests for filter segment functionality (#21160)
This commit is contained in:
@@ -362,7 +362,7 @@ test.describe("Bookings", () => {
|
||||
(response) => response.url().includes("/api/trpc/bookings/get?batch=1") && response.status() === 200
|
||||
);
|
||||
await page
|
||||
.locator(`[data-testid="multi-select-options-userId"] [role="option"]:has-text("${thirdUser.name}")`)
|
||||
.locator(`[data-testid="select-filter-options-userId"] [role="option"]:has-text("${thirdUser.name}")`)
|
||||
.click();
|
||||
await bookingsGetResponse2;
|
||||
await expect(page.locator('text="Cancel event"').nth(0)).toBeVisible();
|
||||
@@ -465,7 +465,7 @@ test.describe("Bookings", () => {
|
||||
await page.locator('[data-testid="add-filter-item-userId"]').click();
|
||||
await page.locator('[data-testid="filter-popover-trigger-userId"]').click();
|
||||
await page
|
||||
.locator(`[data-testid="multi-select-options-userId"] [role="option"]:has-text("${anotherUser}")`)
|
||||
.locator(`[data-testid="select-filter-options-userId"] [role="option"]:has-text("${anotherUser}")`)
|
||||
.click();
|
||||
await page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url()));
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Select a filter from the filter dropdown
|
||||
*/
|
||||
export async function selectFilter(page: Page, columnId: string) {
|
||||
await page.getByTestId("add-filter-button").click();
|
||||
await page.getByTestId(`add-filter-item-${columnId}`).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a filter with a specific value
|
||||
*/
|
||||
export async function applyFilter(page: Page, columnId: string, value: string) {
|
||||
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
|
||||
if (!(await existingFilter.isVisible())) {
|
||||
await selectFilter(page, columnId);
|
||||
}
|
||||
|
||||
await page.getByTestId(`filter-popover-trigger-${columnId}`).click();
|
||||
|
||||
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
export async function clearFilters(page: Page) {
|
||||
await page.getByTestId("clear-filters-button").click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter segment
|
||||
*/
|
||||
export async function createFilterSegment(
|
||||
page: Page,
|
||||
name: string,
|
||||
options: { teamScope?: boolean; teamName?: string } = {}
|
||||
) {
|
||||
await page.getByTestId("save-filter-segment-button").click();
|
||||
|
||||
await page.getByTestId("save-filter-segment-name").fill(name);
|
||||
|
||||
if (options.teamScope) {
|
||||
await page.getByLabel("Save for team").click();
|
||||
if (options.teamName) {
|
||||
await page.getByTestId("save-filter-segment-team-select").click();
|
||||
await page
|
||||
.locator('[data-testid="save-filter-segment-dialog"] [id^="react-select-"]')
|
||||
.getByText(options.teamName)
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
await page.getByTestId("save-filter-segment-dialog").getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await expect(page.getByText("Filter segment saved")).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a segment from the dropdown
|
||||
*/
|
||||
export async function selectSegment(page: Page, segmentName: string) {
|
||||
await page.getByTestId("filter-segment-select").click();
|
||||
|
||||
await page
|
||||
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||
.filter({ hasText: segmentName })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open submenu of a certain segment
|
||||
*/
|
||||
export async function openSegmentSubmenu(page: Page, segmentName: string) {
|
||||
await page.getByTestId("filter-segment-select").click();
|
||||
|
||||
await page
|
||||
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
|
||||
.filter({ hasText: segmentName })
|
||||
.locator('[data-testid="filter-segment-select-submenu-button"]')
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a segment
|
||||
*/
|
||||
export async function deleteSegment(page: Page, segmentName: string) {
|
||||
openSegmentSubmenu(page, segmentName);
|
||||
|
||||
await page.getByTestId("filter-segment-select-submenu-content").getByText("Delete").click();
|
||||
|
||||
await page
|
||||
.locator('[role="dialog"]')
|
||||
.filter({ hasText: "Delete Segment" })
|
||||
.getByRole("button", { name: "Delete" })
|
||||
.click();
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByText("Filter segment deleted")).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available segments
|
||||
*/
|
||||
export async function listSegments(page: Page): Promise<string[]> {
|
||||
await page.getByTestId("filter-segment-select").click();
|
||||
|
||||
const menuItems = page.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]');
|
||||
const count = await menuItems.count();
|
||||
|
||||
const segments: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = await menuItems.nth(i).innerText();
|
||||
segments.push(text);
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
return segments;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import {
|
||||
applyFilter,
|
||||
createFilterSegment,
|
||||
selectSegment,
|
||||
deleteSegment,
|
||||
listSegments,
|
||||
clearFilters,
|
||||
openSegmentSubmenu,
|
||||
} from "./filter-helpers";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, orgs }) => {
|
||||
await users.deleteAll();
|
||||
await orgs.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Filter Segment Functionality", () => {
|
||||
test("Admin can create, use, and delete filter segments in organization members list", async ({
|
||||
page,
|
||||
users,
|
||||
orgs,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const memberUser = await users.create({
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "member-user",
|
||||
});
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "admin-user",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = page.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
const segmentName = "Admin Users";
|
||||
|
||||
await test.step("Can apply and save a role filter as a segment", async () => {
|
||||
await applyFilter(page, "role", "admin");
|
||||
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeHidden();
|
||||
|
||||
await createFilterSegment(page, segmentName);
|
||||
|
||||
await clearFilters(page);
|
||||
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeVisible();
|
||||
|
||||
await selectSegment(page, segmentName);
|
||||
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Can delete a filter segment", async () => {
|
||||
await deleteSegment(page, "Admin Users");
|
||||
|
||||
const segments = await listSegments(page);
|
||||
expect(segments.includes(segmentName)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Filter segments persist across page reloads", async ({ page, users, orgs }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
|
||||
const memberUser = await users.create({
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "member-user",
|
||||
});
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "admin-user",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = page.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await applyFilter(page, "role", "admin");
|
||||
const segmentName = "Admin Users Persistent";
|
||||
await createFilterSegment(page, segmentName);
|
||||
|
||||
await page.reload();
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await selectSegment(page, segmentName);
|
||||
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeHidden();
|
||||
|
||||
await deleteSegment(page, segmentName);
|
||||
});
|
||||
|
||||
test("Admin can create and use team scope filter segments", async ({ page, users, prisma }) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
|
||||
hasSubteam: true,
|
||||
});
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
const { team: subTeam } = await orgOwner.getFirstTeamMembership();
|
||||
|
||||
const memberUser = await users.create({
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "org-member",
|
||||
});
|
||||
|
||||
const adminUser = await users.create({
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
organizationId: org.id,
|
||||
|
||||
username: "org-admin",
|
||||
});
|
||||
|
||||
await orgOwner.apiLogin();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
|
||||
const dataTable = page.getByTestId("user-list-data-table");
|
||||
await expect(dataTable).toBeVisible();
|
||||
const segmentName = "Team Admin Filter";
|
||||
|
||||
await test.step("Can create a team scope filter segment", async () => {
|
||||
await applyFilter(page, "role", "admin");
|
||||
|
||||
await createFilterSegment(page, segmentName, {
|
||||
teamScope: true,
|
||||
teamName: subTeam.name,
|
||||
});
|
||||
|
||||
await clearFilters(page);
|
||||
await selectSegment(page, segmentName);
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Regular member can see but not modify team segments", async () => {
|
||||
const regularMember = await users.create({
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
organizationId: org.id,
|
||||
username: "regular-member",
|
||||
});
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
createdAt: new Date(),
|
||||
teamId: subTeam.id,
|
||||
userId: regularMember.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
await regularMember.apiLogin();
|
||||
|
||||
await page.goto(`/settings/organizations/${org.slug}/members`);
|
||||
await expect(dataTable).toBeVisible();
|
||||
|
||||
await selectSegment(page, "Team Admin Filter");
|
||||
await expect(page.getByText(adminUser.email)).toBeVisible();
|
||||
await expect(page.getByText(memberUser.email)).toBeHidden();
|
||||
|
||||
await openSegmentSubmenu(page, segmentName);
|
||||
await expect(
|
||||
page.getByTestId("filter-segment-select-submenu-content").getByText("Delete")
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -212,13 +212,13 @@ test.describe("Insights", async () => {
|
||||
await page.getByTestId("filter-popover-trigger-bookingUserId").click();
|
||||
|
||||
await page
|
||||
.locator('[data-testid="single-select-options-bookingUserId"]')
|
||||
.locator('[data-testid="select-filter-options-bookingUserId"]')
|
||||
.getByRole("option")
|
||||
.nth(0)
|
||||
.click();
|
||||
|
||||
await page
|
||||
.locator('[data-testid="single-select-options-bookingUserId"]')
|
||||
.locator('[data-testid="select-filter-options-bookingUserId"]')
|
||||
.getByRole("option")
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
@@ -123,7 +123,7 @@ export function BaseSelectFilterOptions<
|
||||
|
||||
return (
|
||||
<Command data-testid={`${testIdPrefix}-${column.id}`}>
|
||||
<CommandInput placeholder={t("search")} />
|
||||
<CommandInput placeholder={t("search")} data-testid={`select-filter-options-search-${column.id}`} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("no_options_available")}</CommandEmpty>
|
||||
{options.map((option, index) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export function MultiSelectFilterOptions({ column }: MultiSelectFilterOptionsPro
|
||||
<BaseSelectFilterOptions<ColumnFilterType.MULTI_SELECT>
|
||||
column={column}
|
||||
filterValueSchema={ZMultiSelectFilterValue}
|
||||
testIdPrefix="multi-select-options"
|
||||
testIdPrefix="select-filter-options"
|
||||
isOptionSelected={(filterValue, optionValue) => {
|
||||
if (!filterValue?.data) return false;
|
||||
return filterValue.data.includes(optionValue);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function SingleSelectFilterOptions({ column }: SingleSelectFilterOptionsP
|
||||
<BaseSelectFilterOptions<ColumnFilterType.SINGLE_SELECT>
|
||||
column={column}
|
||||
filterValueSchema={ZSingleSelectFilterValue}
|
||||
testIdPrefix="single-select-options"
|
||||
testIdPrefix="select-filter-options"
|
||||
isOptionSelected={(filterValue, optionValue) => filterValue?.data === optionValue}
|
||||
onOptionSelect={(column, filterValue, optionValue) => {
|
||||
updateFilter(column.id, { type: ColumnFilterType.SINGLE_SELECT, data: optionValue });
|
||||
|
||||
@@ -111,12 +111,16 @@ export function FilterSegmentSelect() {
|
||||
<>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button color="secondary" StartIcon="list-filter" EndIcon="chevron-down">
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon="list-filter"
|
||||
EndIcon="chevron-down"
|
||||
data-testid="filter-segment-select">
|
||||
{selectedSegment?.name || t("segment")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="start" className="w-60">
|
||||
<DropdownMenuContent align="start" className="w-60" data-testid="filter-segment-select-content">
|
||||
{segmentGroups.length === 0 && <p className="text-subtle px-3 py-1">{t("no_segments")}</p>}
|
||||
|
||||
{segmentGroups.map((group, index) => (
|
||||
@@ -198,10 +202,24 @@ function DropdownItemWithSubmenu({
|
||||
<div className="grow" />
|
||||
<Dropdown open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon="ellipsis" variant="icon" color="minimal" className="rounded-full" />
|
||||
<Button
|
||||
StartIcon="ellipsis"
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
className="rounded-full"
|
||||
data-testid="filter-segment-select-submenu-button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setIsOpen((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="start" side="right" sideOffset={8}>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
data-testid="filter-segment-select-submenu-content">
|
||||
{filteredSubmenuItems.map((item, index) => (
|
||||
<DropdownMenuItem key={index}>
|
||||
<DropdownItem
|
||||
|
||||
@@ -173,11 +173,15 @@ export function SaveFilterSegmentButton() {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button StartIcon="bookmark" color="secondary" disabled={!canSaveSegment}>
|
||||
<Button
|
||||
StartIcon="bookmark"
|
||||
color="secondary"
|
||||
disabled={!canSaveSegment}
|
||||
data-testid="save-filter-segment-button">
|
||||
{t("save")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="save-filter-segment-dialog">
|
||||
<DialogHeader title={t("save_segment")} />
|
||||
<Form form={form} handleSubmit={onSubmit}>
|
||||
{selectedSegment ? (
|
||||
@@ -200,7 +204,7 @@ export function SaveFilterSegmentButton() {
|
||||
{saveMode === "create" && (
|
||||
<div>
|
||||
<Label>{t("name")}</Label>
|
||||
<Input {...form.register("name")} required />
|
||||
<Input {...form.register("name")} data-testid="save-filter-segment-name" required />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -225,6 +229,7 @@ export function SaveFilterSegmentButton() {
|
||||
}))}
|
||||
onChange={(option) => setSelectedTeamId(parseInt(option?.value || "0"))}
|
||||
placeholder={t("select_team")}
|
||||
data-testid="save-filter-segment-team-select"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user