Compare commits
3 Commits
main
...
teste2e-wo
Author | SHA1 | Date | |
---|---|---|---|
|
a3ef648881 | ||
|
a696cd6e01 | ||
|
994ab9946d |
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user