From ca3e3600f74de627ac229a7c5a617ad27b54ecbc Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:11:36 -0500 Subject: [PATCH] test: Check the workflow tab and your functionalities (teste2e-workflow) (#12474) * test: Check the workflow tab * fix type --------- Co-authored-by: gitstart-calcom Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: CarinaWolli --- .../playwright/fixtures/regularBookings.ts | 9 ++ apps/web/playwright/fixtures/users.ts | 41 +++++- apps/web/playwright/fixtures/workflows.ts | 131 ++++++++++++++++++ apps/web/playwright/lib/fixtures.ts | 6 + apps/web/playwright/lib/testUtils.ts | 5 +- apps/web/playwright/workflow.e2e.ts | 129 +++++++++-------- .../collective-scheduling.test.ts | 2 - .../workflows/components/WorkflowListPage.tsx | 4 + 8 files changed, 256 insertions(+), 71 deletions(-) create mode 100644 apps/web/playwright/fixtures/workflows.ts diff --git a/apps/web/playwright/fixtures/regularBookings.ts b/apps/web/playwright/fixtures/regularBookings.ts index 18f2f0a5d5..c676a67e33 100644 --- a/apps/web/playwright/fixtures/regularBookings.ts +++ b/apps/web/playwright/fixtures/regularBookings.ts @@ -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()) { diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index cc7473c725..c14e0e58f9 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -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; const userIncludes = PrismaType.validator()({ eventTypes: true, + workflows: true, credentials: true, routingForms: true, }); @@ -42,6 +43,19 @@ const seededForm = { type UserWithIncludes = PrismaType.UserGetPayload; +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 }, @@ -123,6 +137,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; @@ -173,6 +188,7 @@ export const createUsersFixture = ( scenario: { seedRoutingForms?: boolean; hasTeam?: true; + teamRole?: MembershipRole; teammates?: CustomUserOpts[]; schedulingType?: SchedulingType; teamEventTitle?: string; @@ -204,6 +220,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: { @@ -327,7 +355,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" @@ -569,6 +605,7 @@ type CustomUserOptsKeys = type CustomUserOpts = Partial> & { timeZone?: TimeZoneEnum; eventTypes?: SupportedTestEventTypes[]; + workflows?: SupportedTestWorkflows[]; // ignores adding the worker-index after username useExactUsername?: boolean; roleInOrganization?: MembershipRole; diff --git a/apps/web/playwright/fixtures/workflows.ts b/apps/web/playwright/fixtures/workflows.ts new file mode 100644 index 0000000000..d013134a2f --- /dev/null +++ b/apps/web/playwright/fixtures/workflows.ts @@ -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, + }; +} diff --git a/apps/web/playwright/lib/fixtures.ts b/apps/web/playwright/lib/fixtures.ts index 27182300ef..af6ad825ab 100644 --- a/apps/web/playwright/lib/fixtures.ts +++ b/apps/web/playwright/lib/fixtures.ts @@ -15,6 +15,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; @@ -28,6 +29,7 @@ export interface Fixtures { emails: ReturnType; routingForms: ReturnType; bookingPage: ReturnType; + workflowPage: ReturnType; features: ReturnType; eventTypePage: ReturnType; } @@ -94,6 +96,10 @@ export const test = base.extend({ await features.init(); await use(features); }, + workflowPage: async ({ page }, use) => { + const workflowPage = createWorkflowPageFixture(page); + await use(workflowPage); + }, eventTypePage: async ({ page }, use) => { const eventTypePage = createEventTypeFixture(page); await use(eventTypePage); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 1bccb632d9..bafa7ef90b 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -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); diff --git a/apps/web/playwright/workflow.e2e.ts b/apps/web/playwright/workflow.e2e.ts index 5fc176bb65..c286e6c84f 100644 --- a/apps/web/playwright/workflow.e2e.ts +++ b/apps/web/playwright/workflow.e2e.ts @@ -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(); + }); + }); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index a7a444df58..2afa6d6361 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -3,10 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; - import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; - import { ErrorCode } from "@calcom/lib/errorCodes"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; diff --git a/packages/features/ee/workflows/components/WorkflowListPage.tsx b/packages/features/ee/workflows/components/WorkflowListPage.tsx index 2b4f37ce68..2e0c9b9507 100644 --- a/packages/features/ee/workflows/components/WorkflowListPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowListPage.tsx @@ -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 (
  • {!(firstItem && firstItem.id === workflow.id) && ( 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" /> @@ -246,6 +249,7 @@ export default function WorkflowListPage({ workflows }: Props) { variant="icon" disabled={workflow.readOnly} StartIcon={Trash2} + data-testid="delete-button" />