Compare commits

...

3 Commits

Author SHA1 Message Date
GitStart-Cal.com a3ef648881
Merge branch 'main' into teste2e-workflow 2024-01-09 17:31:40 +05:45
gitstart-calcom a696cd6e01 test: Check the workflow tab 2023-11-21 21:07:11 +00:00
gitstart-calcom 994ab9946d test: Check the workflow tab 2023-11-21 20:58:38 +00:00
7 changed files with 257 additions and 69 deletions

View File

@ -1,6 +1,7 @@
import { expect, type Page } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import type { MembershipRole } from "@calcom/prisma/enums";
import { localize } from "../lib/testUtils";
import type { createUsersFixture } from "./users";
@ -179,6 +180,14 @@ export async function loginUser(users: UserFixture) {
await pro.apiLogin();
}
export async function loginUserWithTeam(users: UserFixture, role: MembershipRole) {
const pro = await users.create(
{ name: "testuser" },
{ hasTeam: true, teamRole: role, isOrg: true, hasSubteam: true }
);
await pro.apiLogin();
}
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
try {
if (isLastDayOfMonth()) {

View File

@ -9,7 +9,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { MembershipRole, SchedulingType, TimeUnit, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { Schedule } from "@calcom/types/schedule";
@ -27,6 +27,7 @@ type UserFixture = ReturnType<typeof createUserFixture>;
const userIncludes = PrismaType.validator<PrismaType.UserInclude>()({
eventTypes: true,
workflows: true,
credentials: true,
routingForms: true,
});
@ -42,6 +43,19 @@ const seededForm = {
type UserWithIncludes = PrismaType.UserGetPayload<typeof userWithEventTypes>;
const createTeamWorkflow = async (user: { id: number }, team: { id: number }) => {
return await prisma.workflow.create({
data: {
name: "Team Workflow",
trigger: WorkflowTriggerEvents.BEFORE_EVENT,
time: 24,
timeUnit: TimeUnit.HOUR,
userId: user.id,
teamId: team.id,
},
});
};
const createTeamEventType = async (
user: { id: number },
team: { id: number },
@ -120,6 +134,7 @@ const createTeamAndAddUser = async (
if (isOrg && hasSubteam) {
const team = await createTeamAndAddUser({ user }, workerInfo);
await createTeamEventType(user, team);
await createTeamWorkflow(user, team);
data.children = { connect: [{ id: team.id }] };
}
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
@ -170,6 +185,7 @@ export const createUsersFixture = (
scenario: {
seedRoutingForms?: boolean;
hasTeam?: true;
teamRole?: MembershipRole;
teammates?: CustomUserOpts[];
schedulingType?: SchedulingType;
teamEventTitle?: string;
@ -201,6 +217,18 @@ export const createUsersFixture = (
});
}
const workflows: SupportedTestWorkflows[] = [
{ name: "Default Workflow", trigger: "NEW_EVENT" },
{ name: "Test Workflow", trigger: "EVENT_CANCELLED" },
...(opts?.workflows || []),
];
for (const workflowData of workflows) {
workflowData.user = { connect: { id: _user.id } };
await prisma.workflow.create({
data: workflowData,
});
}
if (scenario.seedRoutingForms) {
await prisma.app_RoutingForms_Form.create({
data: {
@ -324,7 +352,12 @@ export const createUsersFixture = (
if (scenario.hasTeam) {
const team = await createTeamAndAddUser(
{
user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
user: {
id: user.id,
email: user.email,
username: user.username,
role: scenario.teamRole || "OWNER",
},
isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg,
isOrgVerified: scenario.isOrgVerified,
@ -555,6 +588,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
type SupportedTestEventTypes = PrismaType.EventTypeCreateInput & {
_bookings?: PrismaType.BookingCreateInput[];
};
type SupportedTestWorkflows = PrismaType.WorkflowCreateInput;
type CustomUserOptsKeys =
| "username"
| "password"
@ -567,6 +603,7 @@ type CustomUserOptsKeys =
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
timeZone?: TimeZoneEnum;
eventTypes?: SupportedTestEventTypes[];
workflows?: SupportedTestWorkflows[];
// ignores adding the worker-index after username
useExactUsername?: boolean;
roleInOrganization?: MembershipRole;

View File

@ -0,0 +1,131 @@
import type { Locator } from "@playwright/test";
import { expect, type Page } from "@playwright/test";
import prisma from "@calcom/prisma";
import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { localize } from "../lib/testUtils";
type CreateWorkflowProps = {
name?: string;
isTeam?: true;
trigger?: WorkflowTriggerEvents;
};
export function createWorkflowPageFixture(page: Page) {
const createWorkflow = async (props: CreateWorkflowProps) => {
const { name, isTeam, trigger } = props;
if (isTeam) {
await page.getByTestId("create-button-dropdown").click();
await page.getByTestId("option-team-1").click();
} else {
await page.getByTestId("create-button").click();
}
if (name) {
await fillNameInput(name);
}
if (trigger) {
page.locator("div").filter({ hasText: WorkflowTriggerEvents.BEFORE_EVENT }).nth(1);
page.getByText(trigger);
await selectEventType("30 min");
}
await saveWorkflow();
};
const saveWorkflow = async () => {
await page.getByTestId("save-workflow").click();
};
const assertListCount = async (count: number) => {
const workflowListCount = await page.locator('[data-testid="workflow-list"] > li');
await expect(workflowListCount).toHaveCount(count);
};
const fillNameInput = async (name: string) => {
await page.getByTestId("workflow-name").fill(name);
};
const editSelectedWorkflow = async (name: string) => {
const selectedWorkflow = page.getByTestId("workflow-list").getByTestId(nameToTestId(name));
const editButton = selectedWorkflow.getByRole("button").nth(0);
await editButton.click();
};
const hasWorkflowInList = async (name: string, negate?: true) => {
const selectedWorkflow = page.getByTestId("workflow-list").getByTestId(nameToTestId(name));
if (negate) {
await expect(selectedWorkflow).toBeHidden();
} else {
await expect(selectedWorkflow).toBeVisible();
}
};
const deleteAndConfirm = async (workflow: Locator) => {
const deleteButton = workflow.getByTestId("delete-button");
const confirmDeleteText = (await localize("en"))("confirm_delete_workflow");
await deleteButton.click();
await page.getByRole("button", { name: confirmDeleteText }).click();
};
const selectEventType = async (name: string) => {
await page.getByText("Select...").click();
await page.getByText(name, { exact: true }).click();
};
const hasReadonlyBadge = async () => {
const readOnlyBadge = page.getByText((await localize("en"))("readonly"));
await expect(readOnlyBadge).toBeVisible();
};
const selectedWorkflowPage = async (name: string) => {
await page.getByTestId("workflow-list").getByTestId(nameToTestId(name)).click();
};
const workflowOptionsAreDisabled = async (workflow: string, negate?: boolean) => {
const getWorkflowButton = async (buttonTestId: string) =>
page.getByTestId(nameToTestId(workflow)).getByTestId(buttonTestId);
const [editButton, deleteButton] = await Promise.all([
getWorkflowButton("edit-button"),
getWorkflowButton("delete-button"),
]);
expect(editButton.isDisabled()).toBeTruthy();
expect(deleteButton.isDisabled()).toBeTruthy();
};
const assertWorkflowReminders = async (eventTypeId: number, count: number) => {
const booking = await prisma.booking.findFirst({
where: {
eventTypeId,
},
});
const workflowReminders = await prisma.workflowReminder.findMany({
where: {
bookingUid: booking?.uid ?? "",
},
});
expect(workflowReminders).toHaveLength(count);
};
function nameToTestId(name: string) {
return `workflow-${name.split(" ").join("-").toLowerCase()}`;
}
return {
createWorkflow,
saveWorkflow,
assertListCount,
fillNameInput,
editSelectedWorkflow,
hasWorkflowInList,
deleteAndConfirm,
selectEventType,
hasReadonlyBadge,
selectedWorkflowPage,
workflowOptionsAreDisabled,
assertWorkflowReminders,
};
}

View File

@ -14,6 +14,7 @@ import { createBookingPageFixture } from "../fixtures/regularBookings";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
import { createWorkflowPageFixture } from "../fixtures/workflows";
export interface Fixtures {
page: Page;
@ -27,6 +28,8 @@ export interface Fixtures {
emails: ReturnType<typeof createEmailsFixture>;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
bookingPage: ReturnType<typeof createBookingPageFixture>;
clipboard: ReturnType<typeof createClipboardFixture>;
workflowPage: ReturnType<typeof createWorkflowPageFixture>;
features: ReturnType<typeof createFeatureFixture>;
}
@ -92,4 +95,8 @@ export const test = base.extend<Fixtures>({
await features.init();
await use(features);
},
workflowPage: async ({ page }, use) => {
const workflowPage = createWorkflowPageFixture(page);
await use(workflowPage);
},
});

View File

@ -113,7 +113,10 @@ export async function selectSecondAvailableTimeSlotNextMonth(page: Page) {
await page.locator('[data-testid="time"]').nth(0).click();
}
async function bookEventOnThisPage(page: Page) {
export async function bookEventOnThisPage(page: Page, goTo?: string) {
if (goTo) {
await page.goto(goTo);
}
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);

View File

@ -1,80 +1,77 @@
import { expect } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { WorkflowMethods } from "@calcom/prisma/enums";
import { MembershipRole, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { loginUser, loginUserWithTeam } from "./fixtures/regularBookings";
import { test } from "./lib/fixtures";
import { selectSecondAvailableTimeSlotNextMonth, todo } from "./lib/testUtils";
import { bookEventOnThisPage } from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
test.describe("Workflow Tab - Event Type", () => {
test.describe("Check the functionalities of the Workflow Tab", () => {
test.describe("User Workflows", () => {
test.beforeEach(async ({ page, users }) => {
await loginUser(users);
await page.goto("/workflows");
});
test.describe("Workflow tests", () => {
test.describe("User Workflows", () => {
// Fixme: This test is failing because the listing isn't immediately updated after the workflow is created
test.fixme(
"Create default reminder workflow & trigger when event type is booked",
async ({ page, users }) => {
const user = await users.create();
test("Creating a new workflow", async ({ workflowPage }) => {
const { createWorkflow, assertListCount } = workflowPage;
await createWorkflow({ name: "" });
await assertListCount(3);
});
test("Editing an existing workflow", async ({ workflowPage }) => {
const { saveWorkflow, fillNameInput, editSelectedWorkflow, hasWorkflowInList } = workflowPage;
await editSelectedWorkflow("Test Workflow");
await fillNameInput("Edited Workflow");
await saveWorkflow();
await hasWorkflowInList("Edited Workflow");
});
test("Deleting an existing workflow", async ({ page, workflowPage }) => {
const { hasWorkflowInList, deleteAndConfirm, assertListCount } = workflowPage;
const firstWorkflow = page
.getByTestId("workflow-list")
.getByTestId(/workflow/)
.first();
await deleteAndConfirm(firstWorkflow);
await hasWorkflowInList("Edited Workflow", true);
await assertListCount(1);
});
test("Create an action and check if workflow is triggered", async ({ page, users, workflowPage }) => {
const { createWorkflow, assertWorkflowReminders } = workflowPage;
const [user] = users.get();
const [eventType] = user.eventTypes;
await user.apiLogin();
await page.goto(`/workflows`);
await page.click('[data-testid="create-button"]');
await createWorkflow({ name: "A New Workflow", trigger: WorkflowTriggerEvents.NEW_EVENT });
await bookEventOnThisPage(page, `/${user.username}/${eventType.slug}`);
await assertWorkflowReminders(eventType.id, 1);
});
});
// select first event type
await page.getByText("Select...").click();
await page.getByText(eventType.title, { exact: true }).click();
test.describe("Team Workflows", () => {
test("Admin user", async ({ page, users, workflowPage }) => {
const { createWorkflow, assertListCount } = workflowPage;
// name workflow
await page.fill('[data-testid="workflow-name"]', "Test workflow");
await loginUserWithTeam(users, MembershipRole.ADMIN);
await page.goto("/workflows");
// save workflow
await page.click('[data-testid="save-workflow"]');
await createWorkflow({ name: "A New Workflow", isTeam: true });
await assertListCount(4);
});
// check if workflow is saved
await expect(page.locator('[data-testid="workflow-list"] > li')).toHaveCount(1);
test("Member user", async ({ page, users, workflowPage }) => {
const { hasReadonlyBadge, selectedWorkflowPage, workflowOptionsAreDisabled } = workflowPage;
// book event type
await page.goto(`/${user.username}/${eventType.slug}`);
await selectSecondAvailableTimeSlotNextMonth(page);
await loginUserWithTeam(users, MembershipRole.MEMBER);
await page.goto("/workflows");
await page.fill('[name="name"]', "Test");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// Make sure booking is completed
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const booking = await prisma.booking.findFirst({
where: {
eventTypeId: eventType.id,
},
});
// check if workflow triggered
const workflowReminders = await prisma.workflowReminder.findMany({
where: {
bookingUid: booking?.uid ?? "",
},
});
expect(workflowReminders).toHaveLength(1);
const scheduledDate = dayjs(booking?.startTime).subtract(1, "day").toDate();
expect(workflowReminders[0].method).toBe(WorkflowMethods.EMAIL);
expect(workflowReminders[0].scheduledDate.toISOString()).toBe(scheduledDate.toISOString());
}
);
// add all other actions to this workflow and test if they triggered
// cancel booking and test if workflow reminders are deleted
// test all other triggers
});
test.describe("Team Workflows", () => {
todo("Admin can create and update team workflow");
todo("Members can not create and update team workflows");
await workflowOptionsAreDisabled("Team Workflow");
await selectedWorkflowPage("Team Workflow");
await hasReadonlyBadge();
});
});
});
});

View File

@ -102,9 +102,11 @@ export default function WorkflowListPage({ workflows }: Props) {
{workflows.map((workflow, index) => {
const firstItem = workflows[0];
const lastItem = workflows[workflows.length - 1];
const dataTestId = `workflow-${workflow.name.toLowerCase().replaceAll(" ", "-")}`;
return (
<li
key={workflow.id}
data-testid={dataTestId}
className="group flex w-full max-w-full items-center justify-between overflow-hidden">
{!(firstItem && firstItem.id === workflow.id) && (
<ArrowButton onClick={() => moveWorkflow(index, -1)} arrowDirection="up" />
@ -234,6 +236,7 @@ export default function WorkflowListPage({ workflows }: Props) {
StartIcon={Edit2}
disabled={workflow.readOnly}
onClick={async () => await router.replace(`/workflows/${workflow.id}`)}
data-testid="edit-button"
/>
</Tooltip>
<Tooltip content={t("delete") as string}>
@ -246,6 +249,7 @@ export default function WorkflowListPage({ workflows }: Props) {
variant="icon"
disabled={workflow.readOnly}
StartIcon={Trash2}
data-testid="delete-button"
/>
</Tooltip>
</ButtonGroup>