Skip to content

Commit 79d6952

Browse files
test: add e2e tests for filter segment functionality (calcom#21160)
1 parent 2dbbdc2 commit 79d6952

9 files changed

Lines changed: 365 additions & 14 deletions

File tree

apps/web/playwright/bookings-list.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ test.describe("Bookings", () => {
362362
(response) => response.url().includes("/api/trpc/bookings/get?batch=1") && response.status() === 200
363363
);
364364
await page
365-
.locator(`[data-testid="multi-select-options-userId"] [role="option"]:has-text("${thirdUser.name}")`)
365+
.locator(`[data-testid="select-filter-options-userId"] [role="option"]:has-text("${thirdUser.name}")`)
366366
.click();
367367
await bookingsGetResponse2;
368368
await expect(page.locator('text="Cancel event"').nth(0)).toBeVisible();
@@ -465,7 +465,7 @@ test.describe("Bookings", () => {
465465
await page.locator('[data-testid="add-filter-item-userId"]').click();
466466
await page.locator('[data-testid="filter-popover-trigger-userId"]').click();
467467
await page
468-
.locator(`[data-testid="multi-select-options-userId"] [role="option"]:has-text("${anotherUser}")`)
468+
.locator(`[data-testid="select-filter-options-userId"] [role="option"]:has-text("${anotherUser}")`)
469469
.click();
470470
await page.waitForResponse((response) => /\/api\/trpc\/bookings\/get.*/.test(response.url()));
471471

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { Page } from "@playwright/test";
2+
import { expect } from "@playwright/test";
3+
4+
/**
5+
* Select a filter from the filter dropdown
6+
*/
7+
export async function selectFilter(page: Page, columnId: string) {
8+
await page.getByTestId("add-filter-button").click();
9+
await page.getByTestId(`add-filter-item-${columnId}`).click();
10+
}
11+
12+
/**
13+
* Apply a filter with a specific value
14+
*/
15+
export async function applyFilter(page: Page, columnId: string, value: string) {
16+
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
17+
if (!(await existingFilter.isVisible())) {
18+
await selectFilter(page, columnId);
19+
}
20+
21+
await page.getByTestId(`filter-popover-trigger-${columnId}`).click();
22+
23+
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
24+
25+
await page.keyboard.press("Escape");
26+
}
27+
28+
/**
29+
* Clear all filters
30+
*/
31+
export async function clearFilters(page: Page) {
32+
await page.getByTestId("clear-filters-button").click();
33+
}
34+
35+
/**
36+
* Create a filter segment
37+
*/
38+
export async function createFilterSegment(
39+
page: Page,
40+
name: string,
41+
options: { teamScope?: boolean; teamName?: string } = {}
42+
) {
43+
await page.getByTestId("save-filter-segment-button").click();
44+
45+
await page.getByTestId("save-filter-segment-name").fill(name);
46+
47+
if (options.teamScope) {
48+
await page.getByLabel("Save for team").click();
49+
if (options.teamName) {
50+
await page.getByTestId("save-filter-segment-team-select").click();
51+
await page
52+
.locator('[data-testid="save-filter-segment-dialog"] [id^="react-select-"]')
53+
.getByText(options.teamName)
54+
.click();
55+
}
56+
}
57+
58+
await page.getByTestId("save-filter-segment-dialog").getByRole("button", { name: "Save" }).click();
59+
60+
await expect(page.getByText("Filter segment saved")).toBeVisible();
61+
}
62+
63+
/**
64+
* Select a segment from the dropdown
65+
*/
66+
export async function selectSegment(page: Page, segmentName: string) {
67+
await page.getByTestId("filter-segment-select").click();
68+
69+
await page
70+
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
71+
.filter({ hasText: segmentName })
72+
.click();
73+
}
74+
75+
/**
76+
* Open submenu of a certain segment
77+
*/
78+
export async function openSegmentSubmenu(page: Page, segmentName: string) {
79+
await page.getByTestId("filter-segment-select").click();
80+
81+
await page
82+
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
83+
.filter({ hasText: segmentName })
84+
.locator('[data-testid="filter-segment-select-submenu-button"]')
85+
.click();
86+
}
87+
88+
/**
89+
* Delete a segment
90+
*/
91+
export async function deleteSegment(page: Page, segmentName: string) {
92+
openSegmentSubmenu(page, segmentName);
93+
94+
await page.getByTestId("filter-segment-select-submenu-content").getByText("Delete").click();
95+
96+
await page
97+
.locator('[role="dialog"]')
98+
.filter({ hasText: "Delete Segment" })
99+
.getByRole("button", { name: "Delete" })
100+
.click();
101+
102+
await page.keyboard.press("Escape");
103+
await expect(page.getByText("Filter segment deleted")).toBeVisible();
104+
}
105+
106+
/**
107+
* List all available segments
108+
*/
109+
export async function listSegments(page: Page): Promise<string[]> {
110+
await page.getByTestId("filter-segment-select").click();
111+
112+
const menuItems = page.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]');
113+
const count = await menuItems.count();
114+
115+
const segments: string[] = [];
116+
for (let i = 0; i < count; i++) {
117+
const text = await menuItems.nth(i).innerText();
118+
segments.push(text);
119+
}
120+
121+
await page.keyboard.press("Escape");
122+
return segments;
123+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { expect } from "@playwright/test";
2+
3+
import { MembershipRole } from "@calcom/prisma/enums";
4+
5+
import {
6+
applyFilter,
7+
createFilterSegment,
8+
selectSegment,
9+
deleteSegment,
10+
listSegments,
11+
clearFilters,
12+
openSegmentSubmenu,
13+
} from "./filter-helpers";
14+
import { test } from "./lib/fixtures";
15+
16+
test.describe.configure({ mode: "parallel" });
17+
18+
test.afterEach(async ({ users, orgs }) => {
19+
await users.deleteAll();
20+
await orgs.deleteAll();
21+
});
22+
23+
test.describe("Filter Segment Functionality", () => {
24+
test("Admin can create, use, and delete filter segments in organization members list", async ({
25+
page,
26+
users,
27+
orgs,
28+
}) => {
29+
const orgOwner = await users.create(undefined, {
30+
hasTeam: true,
31+
isOrg: true,
32+
});
33+
const { team: org } = await orgOwner.getOrgMembership();
34+
35+
const memberUser = await users.create({
36+
roleInOrganization: MembershipRole.MEMBER,
37+
organizationId: org.id,
38+
39+
username: "member-user",
40+
});
41+
42+
const adminUser = await users.create({
43+
roleInOrganization: MembershipRole.ADMIN,
44+
organizationId: org.id,
45+
46+
username: "admin-user",
47+
});
48+
49+
await orgOwner.apiLogin();
50+
51+
await page.goto(`/settings/organizations/${org.slug}/members`);
52+
53+
const dataTable = page.getByTestId("user-list-data-table");
54+
await expect(dataTable).toBeVisible();
55+
56+
const segmentName = "Admin Users";
57+
58+
await test.step("Can apply and save a role filter as a segment", async () => {
59+
await applyFilter(page, "role", "admin");
60+
61+
await expect(page.getByText(adminUser.email)).toBeVisible();
62+
await expect(page.getByText(memberUser.email)).toBeHidden();
63+
64+
await createFilterSegment(page, segmentName);
65+
66+
await clearFilters(page);
67+
68+
await expect(page.getByText(adminUser.email)).toBeVisible();
69+
await expect(page.getByText(memberUser.email)).toBeVisible();
70+
71+
await selectSegment(page, segmentName);
72+
73+
await expect(page.getByText(adminUser.email)).toBeVisible();
74+
await expect(page.getByText(memberUser.email)).toBeHidden();
75+
});
76+
77+
await test.step("Can delete a filter segment", async () => {
78+
await deleteSegment(page, "Admin Users");
79+
80+
const segments = await listSegments(page);
81+
expect(segments.includes(segmentName)).toBe(false);
82+
});
83+
});
84+
85+
test("Filter segments persist across page reloads", async ({ page, users, orgs }) => {
86+
const orgOwner = await users.create(undefined, {
87+
hasTeam: true,
88+
isOrg: true,
89+
});
90+
const { team: org } = await orgOwner.getOrgMembership();
91+
92+
const memberUser = await users.create({
93+
roleInOrganization: MembershipRole.MEMBER,
94+
organizationId: org.id,
95+
96+
username: "member-user",
97+
});
98+
99+
const adminUser = await users.create({
100+
roleInOrganization: MembershipRole.ADMIN,
101+
organizationId: org.id,
102+
103+
username: "admin-user",
104+
});
105+
106+
await orgOwner.apiLogin();
107+
108+
await page.goto(`/settings/organizations/${org.slug}/members`);
109+
110+
const dataTable = page.getByTestId("user-list-data-table");
111+
await expect(dataTable).toBeVisible();
112+
113+
await applyFilter(page, "role", "admin");
114+
const segmentName = "Admin Users Persistent";
115+
await createFilterSegment(page, segmentName);
116+
117+
await page.reload();
118+
await expect(dataTable).toBeVisible();
119+
120+
await selectSegment(page, segmentName);
121+
122+
await expect(page.getByText(adminUser.email)).toBeVisible();
123+
await expect(page.getByText(memberUser.email)).toBeHidden();
124+
125+
await deleteSegment(page, segmentName);
126+
});
127+
128+
test("Admin can create and use team scope filter segments", async ({ page, users, prisma }) => {
129+
const orgOwner = await users.create(undefined, {
130+
hasTeam: true,
131+
isOrg: true,
132+
133+
hasSubteam: true,
134+
});
135+
const { team: org } = await orgOwner.getOrgMembership();
136+
const { team: subTeam } = await orgOwner.getFirstTeamMembership();
137+
138+
const memberUser = await users.create({
139+
roleInOrganization: MembershipRole.MEMBER,
140+
organizationId: org.id,
141+
142+
username: "org-member",
143+
});
144+
145+
const adminUser = await users.create({
146+
roleInOrganization: MembershipRole.ADMIN,
147+
organizationId: org.id,
148+
149+
username: "org-admin",
150+
});
151+
152+
await orgOwner.apiLogin();
153+
154+
await page.goto(`/settings/organizations/${org.slug}/members`);
155+
156+
const dataTable = page.getByTestId("user-list-data-table");
157+
await expect(dataTable).toBeVisible();
158+
const segmentName = "Team Admin Filter";
159+
160+
await test.step("Can create a team scope filter segment", async () => {
161+
await applyFilter(page, "role", "admin");
162+
163+
await createFilterSegment(page, segmentName, {
164+
teamScope: true,
165+
teamName: subTeam.name,
166+
});
167+
168+
await clearFilters(page);
169+
await selectSegment(page, segmentName);
170+
await expect(page.getByText(adminUser.email)).toBeVisible();
171+
await expect(page.getByText(memberUser.email)).toBeHidden();
172+
});
173+
174+
await test.step("Regular member can see but not modify team segments", async () => {
175+
const regularMember = await users.create({
176+
roleInOrganization: MembershipRole.MEMBER,
177+
organizationId: org.id,
178+
username: "regular-member",
179+
});
180+
await prisma.membership.create({
181+
data: {
182+
createdAt: new Date(),
183+
teamId: subTeam.id,
184+
userId: regularMember.id,
185+
role: MembershipRole.MEMBER,
186+
accepted: true,
187+
},
188+
});
189+
190+
await regularMember.apiLogin();
191+
192+
await page.goto(`/settings/organizations/${org.slug}/members`);
193+
await expect(dataTable).toBeVisible();
194+
195+
await selectSegment(page, "Team Admin Filter");
196+
await expect(page.getByText(adminUser.email)).toBeVisible();
197+
await expect(page.getByText(memberUser.email)).toBeHidden();
198+
199+
await openSegmentSubmenu(page, segmentName);
200+
await expect(
201+
page.getByTestId("filter-segment-select-submenu-content").getByText("Delete")
202+
).toBeHidden();
203+
});
204+
});
205+
});

apps/web/playwright/insights.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,13 @@ test.describe("Insights", async () => {
212212
await page.getByTestId("filter-popover-trigger-bookingUserId").click();
213213

214214
await page
215-
.locator('[data-testid="single-select-options-bookingUserId"]')
215+
.locator('[data-testid="select-filter-options-bookingUserId"]')
216216
.getByRole("option")
217217
.nth(0)
218218
.click();
219219

220220
await page
221-
.locator('[data-testid="single-select-options-bookingUserId"]')
221+
.locator('[data-testid="select-filter-options-bookingUserId"]')
222222
.getByRole("option")
223223
.nth(1)
224224
.click();

packages/features/data-table/components/filters/BaseSelectFilterOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function BaseSelectFilterOptions<
123123

124124
return (
125125
<Command data-testid={`${testIdPrefix}-${column.id}`}>
126-
<CommandInput placeholder={t("search")} />
126+
<CommandInput placeholder={t("search")} data-testid={`select-filter-options-search-${column.id}`} />
127127
<CommandList>
128128
<CommandEmpty>{t("no_options_available")}</CommandEmpty>
129129
{options.map((option, index) => {

packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function MultiSelectFilterOptions({ column }: MultiSelectFilterOptionsPro
1616
<BaseSelectFilterOptions<ColumnFilterType.MULTI_SELECT>
1717
column={column}
1818
filterValueSchema={ZMultiSelectFilterValue}
19-
testIdPrefix="multi-select-options"
19+
testIdPrefix="select-filter-options"
2020
isOptionSelected={(filterValue, optionValue) => {
2121
if (!filterValue?.data) return false;
2222
return filterValue.data.includes(optionValue);

packages/features/data-table/components/filters/SingleSelectFilterOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function SingleSelectFilterOptions({ column }: SingleSelectFilterOptionsP
1616
<BaseSelectFilterOptions<ColumnFilterType.SINGLE_SELECT>
1717
column={column}
1818
filterValueSchema={ZSingleSelectFilterValue}
19-
testIdPrefix="single-select-options"
19+
testIdPrefix="select-filter-options"
2020
isOptionSelected={(filterValue, optionValue) => filterValue?.data === optionValue}
2121
onOptionSelect={(column, filterValue, optionValue) => {
2222
updateFilter(column.id, { type: ColumnFilterType.SINGLE_SELECT, data: optionValue });

0 commit comments

Comments
 (0)