test: add e2e tests for filters on /insights/routing (#21332)

* test: add e2e tests for insights routing filters

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: improve stability of insights routing filters e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: address PR feedback for insights routing filters e2e tests

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add explicit page navigation to resolve timeout issues in filters.e2e.ts

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: navigate to routing tab after loading insights page

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix e2e tests

* fix tests

* rename

* fix tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <hey@eunjae.dev>
This commit is contained in:
devin-ai-integration[bot]
2025-05-20 18:23:57 -04:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> eunjae@cal.com <hey@eunjae.dev>
parent bb98143e2e
commit 25246ef8ff
6 changed files with 770 additions and 9 deletions
+50 -3
View File
@@ -9,18 +9,65 @@ export async function selectFilter(page: Page, columnId: string) {
await page.getByTestId(`add-filter-item-${columnId}`).click();
}
export async function openFilter(page: Page, columnId: string) {
await page.getByTestId(`filter-popover-trigger-${columnId}`).click();
}
/**
* Apply a filter with a specific value
*/
export async function applyFilter(page: Page, columnId: string, value: string) {
export async function applySelectFilter(page: Page, columnId: string, value: string) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await selectFilter(page, columnId);
}
await openFilter(page, columnId);
await selectOptionValue(page, columnId, value);
await page.keyboard.press("Escape");
}
await page.getByTestId(`filter-popover-trigger-${columnId}`).click();
export async function selectOptionValue(page: Page, columnId: string, value: string) {
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
}
export async function applyTextFilter(page: Page, columnId: string, operator: string, operand?: string) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await selectFilter(page, columnId);
}
await openFilter(page, columnId);
await page.getByTestId(`text-filter-options-select-${columnId}`).click();
await page
.locator(`[data-testid="text-filter-options-${columnId}"] [id^="react-select-"]`)
.getByText(operator)
.first()
.click();
if (operand) {
await page.keyboard.press("Tab");
await page.keyboard.type(operand);
}
await page.keyboard.press("Enter");
await page.keyboard.press("Escape");
}
export async function applyNumberFilter(page: Page, columnId: string, operator: string, operand: number) {
const existingFilter = page.getByTestId(`filter-popover-trigger-${columnId}`);
if (!(await existingFilter.isVisible())) {
await selectFilter(page, columnId);
}
await openFilter(page, columnId);
await page.getByTestId(`number-filter-options-select-${columnId}`).click();
await page
.locator(`[data-testid="number-filter-options-${columnId}"] [id^="react-select-"]`)
.getByText(operator)
.first()
.click();
await page.keyboard.press("Tab");
await page.keyboard.type(String(operand));
await page.keyboard.press("Enter");
await page.keyboard.press("Escape");
}
+4 -4
View File
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
import { MembershipRole } from "@calcom/prisma/enums";
import {
applyFilter,
applySelectFilter,
createFilterSegment,
selectSegment,
deleteSegment,
@@ -56,7 +56,7 @@ test.describe("Filter Segment Functionality", () => {
const segmentName = "Admin Users";
await test.step("Can apply and save a role filter as a segment", async () => {
await applyFilter(page, "role", "admin");
await applySelectFilter(page, "role", "admin");
await expect(page.getByText(adminUser.email)).toBeVisible();
await expect(page.getByText(memberUser.email)).toBeHidden();
@@ -110,7 +110,7 @@ test.describe("Filter Segment Functionality", () => {
const dataTable = page.getByTestId("user-list-data-table");
await expect(dataTable).toBeVisible();
await applyFilter(page, "role", "admin");
await applySelectFilter(page, "role", "admin");
const segmentName = "Admin Users Persistent";
await createFilterSegment(page, segmentName);
@@ -158,7 +158,7 @@ test.describe("Filter Segment Functionality", () => {
const segmentName = "Team Admin Filter";
await test.step("Can create a team scope filter segment", async () => {
await applyFilter(page, "role", "admin");
await applySelectFilter(page, "role", "admin");
await createFilterSegment(page, segmentName, {
teamScope: true,
@@ -0,0 +1,711 @@
import { expect } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import {
selectFilter,
openFilter,
clearFilters,
applySelectFilter,
applyTextFilter,
applyNumberFilter,
selectOptionValue,
} from "./filter-helpers";
import { test } from "./lib/fixtures";
test.afterEach(({ users }) => users.deleteAll());
test.describe.configure({ mode: "parallel" });
test.describe("Insights > Routing Filters", () => {
test("formId filter: should filter by selected routing form", async ({
page,
users,
routingForms,
prisma,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const formName1 = "Test Form 1";
const form1 = await routingForms.create({
name: formName1,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
const formName2 = "Test Form 2";
const form2 = await routingForms.create({
name: formName2,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Email",
identifier: "email",
required: true,
},
],
});
await page.goto(`/insights/routing`);
await openFilter(page, "formId");
await selectOptionValue(page, "formId", formName1);
await page.keyboard.press("Escape");
await expect(page.getByText(formName1)).toBeVisible();
await expect(page.getByText(formName2)).toBeHidden();
await openFilter(page, "formId");
await selectOptionValue(page, "formId", formName2);
await page.keyboard.press("Escape");
await expect(page.getByText(formName2)).toBeVisible();
await expect(page.getByText(formName1)).toBeHidden();
});
test("bookingUserId filter: should filter by user", async ({ page, users, routingForms, prisma }) => {
const owner = await users.create(
{ name: "owner" },
{
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
}
);
const membership = await owner.getFirstTeamMembership();
const user1Name = "User One";
const user1 = await users.create({ name: user1Name });
const user2 = await users.create({ name: "User Two" });
await prisma.membership.createMany({
data: [
{
teamId: membership.teamId,
userId: user1.id,
role: MembershipRole.MEMBER,
accepted: true,
},
{
teamId: membership.teamId,
userId: user2.id,
role: MembershipRole.MEMBER,
accepted: true,
},
],
});
await owner.apiLogin();
const formName = "Filter User Test Form";
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
const booking1 = await prisma.booking.create({
data: {
uid: `booking-${uuidv4()}`,
title: "Test Booking 1",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: user1.id,
status: BookingStatus.ACCEPTED,
},
});
const booking2 = await prisma.booking.create({
data: {
uid: `booking-${uuidv4()}`,
title: "Test Booking 2",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: user2.id,
status: BookingStatus.ACCEPTED,
},
});
const fieldId = (form.fields as any)[0].id;
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-1",
formId: form.id,
response: {
[fieldId]: {
label: "Name",
value: "John Doe",
},
},
routedToBookingUid: booking1.uid,
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-2",
formId: form.id,
response: {
[fieldId]: {
label: "Name",
value: "Jane Smith",
},
},
routedToBookingUid: booking2.uid,
},
});
await page.goto(`/insights/routing`);
await applySelectFilter(page, "bookingUserId", user1Name);
await expect(page.getByText("John Doe")).toBeVisible();
await expect(page.getByText("Jane Smith")).toBeHidden();
await prisma.booking.delete({ where: { id: booking1.id } });
await prisma.booking.delete({ where: { id: booking2.id } });
});
// We will add it back when we enable the bookingAttendees filter on /insights/routing
test.skip("bookingAttendees filter: should filter by attendee name or email", async ({
page,
users,
routingForms,
prisma,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const formName = "Attendee Filter Test Form";
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
const fieldId = (form.fields as any)[0].id;
const booking1 = await prisma.booking.create({
data: {
uid: `booking-${uuidv4()}`,
title: "Test Booking 1",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: owner.id,
status: BookingStatus.ACCEPTED,
attendees: {
create: [
{
email: "john.doe@example.com",
name: "John Doe",
timeZone: "UTC",
},
],
},
},
});
const booking2 = await prisma.booking.create({
data: {
uid: `booking-${uuidv4()}`,
title: "Test Booking 2",
startTime: new Date(),
endTime: new Date(Date.now() + 60 * 60 * 1000),
userId: owner.id,
status: BookingStatus.ACCEPTED,
attendees: {
create: [
{
email: "jane.smith@example.com",
name: "Jane Smith",
timeZone: "UTC",
},
],
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-1",
formId: form.id,
response: { [fieldId]: { label: "Name", value: "Response 1" } },
routedToBookingUid: booking1.uid,
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-2",
formId: form.id,
response: { [fieldId]: { label: "Name", value: "Response 2" } },
routedToBookingUid: booking2.uid,
},
});
await page.goto(`/insights/routing`);
await selectFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("John");
await page.keyboard.press("Enter");
await expect(page.getByText("Response 1")).toBeVisible();
await expect(page.getByText("Response 2")).toBeHidden();
await clearFilters(page);
await selectFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("jane.smith@example");
await page.keyboard.press("Enter");
await expect(page.getByText("Response 2")).toBeVisible();
await expect(page.getByText("Response 1")).toBeHidden();
await clearFilters(page);
await selectFilter(page, "bookingAttendees");
await openFilter(page, "bookingAttendees");
await page.getByPlaceholder("Filter attendees by name or email").fill("JANE");
await page.keyboard.press("Enter");
await expect(page.getByText("Response 2")).toBeVisible();
await expect(page.getByText("Response 1")).toBeHidden();
await prisma.booking.delete({ where: { id: booking1.id } });
await prisma.booking.delete({ where: { id: booking2.id } });
});
test("TEXT field: should filter with different text operators", async ({
page,
users,
routingForms,
prisma,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const textFieldId = uuidv4();
const formName = "Text Filter Test Form";
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Description",
identifier: "description",
required: true,
},
],
});
const fieldId = (form.fields as any)[0].id;
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-1",
formId: form.id,
response: {
[fieldId]: {
label: "Description",
value: "This is a test description",
},
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-2",
formId: form.id,
response: {
[fieldId]: {
label: "Description",
value: "Another description for testing",
},
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-3",
formId: form.id,
response: {
[fieldId]: {
label: "Description",
value: "",
},
},
},
});
await page.goto(`/insights/routing`);
await applyTextFilter(page, fieldId, "Contains", "test");
await expect(page.getByText("This is a test description")).toBeVisible();
await expect(page.getByText("Another description for testing")).toBeVisible();
await clearFilters(page);
await applyTextFilter(page, fieldId, "Is", "This is a test description");
await expect(page.getByText("This is a test description")).toBeVisible();
await expect(page.getByText("Another description for testing")).toBeHidden();
await clearFilters(page);
await applyTextFilter(page, fieldId, "Is empty");
await expect(page.getByText("This is a test description")).toBeHidden();
await expect(page.getByText("Another description for testing")).toBeHidden();
await clearFilters(page);
await applyTextFilter(page, fieldId, "Contains", "TEST");
await expect(page.getByText("This is a test description")).toBeVisible();
await expect(page.getByText("Another description for testing")).toBeVisible();
});
test("NUMBER field: should filter with different number operators", async ({
page,
users,
routingForms,
prisma,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const formName = "Number Filter Test Form";
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "number",
label: "Rating",
identifier: "rating",
required: true,
},
],
});
const fieldId = (form.fields as any)[0].id;
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-1",
formId: form.id,
response: {
[fieldId]: {
label: "Rating",
value: 1,
},
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-2",
formId: form.id,
response: {
[fieldId]: {
label: "Rating",
value: 3,
},
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-3",
formId: form.id,
response: {
[fieldId]: {
label: "Rating",
value: 5,
},
},
},
});
await page.goto(`/insights/routing`);
await applyNumberFilter(page, fieldId, "=", 3);
const getByNumber = (number: number) =>
page.locator(`[data-testid="data-table-td-${fieldId}"]`).getByText(String(number));
await expect(getByNumber(3)).toBeVisible();
await expect(getByNumber(1)).toBeHidden();
await expect(getByNumber(5)).toBeHidden();
await clearFilters(page);
await applyNumberFilter(page, fieldId, ">", 3);
await expect(getByNumber(5)).toBeVisible();
await expect(getByNumber(1)).toBeHidden();
await expect(getByNumber(3)).toBeHidden();
await clearFilters(page);
await applyNumberFilter(page, fieldId, "≤", 3);
await expect(getByNumber(1)).toBeVisible();
await expect(getByNumber(3)).toBeVisible();
await expect(getByNumber(5)).toBeHidden();
});
test("SINGLE_SELECT and MULTI_SELECT fields: should filter correctly", async ({
page,
users,
routingForms,
prisma,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const singleSelectFieldId = uuidv4();
const multiSelectFieldId = uuidv4();
const formName = "Select Filter Test Form";
const locationOptionId1 = uuidv4();
const locationOptionId2 = uuidv4();
const skillOptionId1 = uuidv4();
const skillOptionId2 = uuidv4();
const skillOptionId3 = uuidv4();
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "select",
label: "Location",
identifier: "location",
required: true,
options: [
{
id: locationOptionId1,
label: "New York",
},
{
id: locationOptionId2,
label: "London",
},
],
},
{
type: "multiselect",
label: "Skills",
identifier: "skills",
required: true,
options: [
{
id: skillOptionId1,
label: "JavaScript",
},
{
id: skillOptionId2,
label: "TypeScript",
},
{
id: skillOptionId3,
label: "React",
},
],
},
],
});
const locationFieldId = (form.fields as any)[0].id;
const skillFieldId = (form.fields as any)[1].id;
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-1",
formId: form.id,
response: {
[locationFieldId]: {
label: "Location",
value: locationOptionId1,
},
[skillFieldId]: {
label: "Skills",
value: [skillOptionId1, skillOptionId2],
},
},
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-2",
formId: form.id,
response: {
[locationFieldId]: {
label: "Location",
value: locationOptionId2,
},
[skillFieldId]: {
label: "Skills",
value: [skillOptionId2, skillOptionId3],
},
},
},
});
await page.goto(`/insights/routing`);
const getByColumnValue = (columnId: string, value: string) =>
page.locator(`[data-testid="data-table-td-${columnId}"]`).getByText(value);
await applySelectFilter(page, locationFieldId, "New York");
await expect(getByColumnValue(locationFieldId, "New York")).toBeVisible();
await expect(getByColumnValue(locationFieldId, "London")).toBeHidden();
await clearFilters(page);
await applySelectFilter(page, skillFieldId, "JavaScript");
await applySelectFilter(page, skillFieldId, "TypeScript");
await expect(getByColumnValue(skillFieldId, "JavaScript")).toBeVisible();
await expect(getByColumnValue(skillFieldId, "TypeScript")).toBeVisible();
await expect(getByColumnValue(skillFieldId, "React")).toBeHidden();
});
test("createdAt filter: should filter by date range", async ({ page, users, routingForms, prisma }) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const formName = "Date Range Filter Test Form";
const form = await routingForms.create({
name: formName,
userId: owner.id,
teamId: membership.teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
const fieldId = (form.fields as any)[0].id;
const oldDate = new Date();
oldDate.setDate(oldDate.getDate() - 10);
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-old",
formId: form.id,
response: {
[fieldId]: {
label: "Name",
value: "Old Response",
},
},
createdAt: oldDate,
},
});
await prisma.app_RoutingForms_FormResponse.create({
data: {
formFillerId: "test-filler-recent",
formId: form.id,
response: {
[fieldId]: {
label: "Name",
value: "Recent Response",
},
},
},
});
await page.goto(`/insights/routing`);
await openFilter(page, "createdAt");
await page.getByTestId("date-range-options-w").click(); // Last 7 Days
await page.keyboard.press("Escape");
await expect(page.getByText("Recent Response")).toBeVisible();
await expect(page.getByText("Old Response")).toBeHidden();
await openFilter(page, "createdAt");
await page.getByTestId("date-range-options-t").click(); // Last 30 Days
await page.keyboard.press("Escape");
await expect(page.getByText("Recent Response")).toBeVisible();
await expect(page.getByText("Old Response")).toBeVisible();
});
});
@@ -312,6 +312,7 @@ function DataTableBody<TData>({
return (
<TableCell
key={cell.id}
data-testid={`data-table-td-${cell.column.id}`}
style={{
...(column.getIsPinned() === "left" && { left: `${column.getStart("left")}px` }),
...(column.getIsPinned() === "right" && { right: `${column.getStart("right")}px` }),
@@ -30,7 +30,7 @@ export function NumberFilterOptions({ column }: NumberFilterOptionsProps) {
});
return (
<div className="mx-3 my-2">
<div className="mx-3 my-2" data-testid={`number-filter-options-${column.id}`}>
<Form
form={form}
handleSubmit={({ operatorOption, operand }) => {
@@ -51,6 +51,7 @@ export function NumberFilterOptions({ column }: NumberFilterOptionsProps) {
render={({ field: { value } }) => (
<div className="flex items-center gap-2">
<Select
data-testid={`number-filter-options-select-${column.id}`}
className="basis-1/3"
options={numberFilterOperatorOptions}
value={value}
@@ -31,7 +31,7 @@ export function TextFilterOptions({ column }: TextFilterOptionsProps) {
});
return (
<div className="mx-3 my-2">
<div className="mx-3 my-2" data-testid={`text-filter-options-${column.id}`}>
<Form
form={form}
handleSubmit={({ operatorOption, operand }) => {
@@ -52,6 +52,7 @@ export function TextFilterOptions({ column }: TextFilterOptionsProps) {
render={({ field: { value } }) => (
<>
<Select
data-testid={`text-filter-options-select-${column.id}`}
options={textFilterOperatorOptions}
value={value}
isSearchable={false}