test: add e2e tests for filter segment functionality (#21160)

This commit is contained in:
devin-ai-integration[bot]
2025-05-12 12:52:54 -04:00
committed by GitHub
parent 2dbbdc21f2
commit 79d695236f
9 changed files with 365 additions and 14 deletions
+2 -2
View File
@@ -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()));
+123
View File
@@ -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;
}
+205
View File
@@ -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();
});
});
});
+2 -2
View File
@@ -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>