E2E fixtures (#2747)
This commit is contained in:
@@ -16,11 +16,12 @@ interface AppCardProps {
|
||||
export default function AppCard(props: AppCardProps) {
|
||||
return (
|
||||
<Link href={"/apps/" + props.slug}>
|
||||
<a className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50">
|
||||
<a
|
||||
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
||||
data-testid={`app-store-app-card-${props.slug}`}>
|
||||
<div className="flex">
|
||||
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
<Button
|
||||
data-testid={`app-store-app-card-${props.slug}`}
|
||||
color="secondary"
|
||||
className="ml-auto flex self-start"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -12,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const userId = req.session.user.id;
|
||||
try {
|
||||
const installedApp = await prisma?.credential.findFirst({
|
||||
const installedApp = await prisma.credential.findFirst({
|
||||
where: {
|
||||
type: appCredentialType as string,
|
||||
userId: userId,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
test("Can reset forgotten password", async ({ page }) => {
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test("Can reset forgotten password", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const newPassword = `${user.username!}-123`;
|
||||
// Got to reset password flow
|
||||
await page.goto("/auth/forgot-password");
|
||||
|
||||
await page.waitForSelector("text=Forgot Password");
|
||||
// Fill [placeholder="john.doe@example.com"]
|
||||
await page.fill('input[name="email"]', "pro@example.com");
|
||||
await page.fill('input[name="email"]', `${user.username}@example.com`);
|
||||
|
||||
// Press Enter
|
||||
await Promise.all([
|
||||
@@ -18,7 +23,7 @@ test("Can reset forgotten password", async ({ page }) => {
|
||||
// Wait for page to fully load
|
||||
await page.waitForSelector("text=Reset Password");
|
||||
// Fill input[name="password"]
|
||||
await page.fill('input[name="password"]', "pro");
|
||||
await page.fill('input[name="password"]', newPassword);
|
||||
|
||||
// Click text=Submit
|
||||
await page.click('button[type="submit"]');
|
||||
@@ -33,10 +38,12 @@ test("Can reset forgotten password", async ({ page }) => {
|
||||
await Promise.all([page.waitForNavigation({ url: "/auth/login" }), page.click('button:has-text("Login")')]);
|
||||
|
||||
// Fill input[name="email"]
|
||||
await page.fill('input[name="email"]', "pro@example.com");
|
||||
await page.fill('input[name="password"]', "pro");
|
||||
await page.fill('input[name="email"]', `${user.username}@example.com`);
|
||||
await page.fill('input[name="password"]', newPassword);
|
||||
await page.press('input[name="password"]', "Enter");
|
||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||
|
||||
await expect(page.locator("[data-testid=dashboard-shell]")).toBeVisible();
|
||||
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { deleteAllBookingsByEmail } from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
bookFirstEvent,
|
||||
bookTimeSlot,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
selectSecondAvailableTimeSlotNextMonth,
|
||||
todo,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("free user", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/free");
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const free = await users.create({ plan: "FREE" });
|
||||
await page.goto(`/${free.username}`);
|
||||
});
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// delete test bookings
|
||||
await deleteAllBookingsByEmail("free@example.com");
|
||||
});
|
||||
|
||||
test("only one visible event", async ({ page }) => {
|
||||
await expect(page.locator(`[href="/free/30min"]`)).toBeVisible();
|
||||
await expect(page.locator(`[href="/free/60min"]`)).not.toBeVisible();
|
||||
test("only one visible event", async ({ page, users }) => {
|
||||
const [free] = users.get();
|
||||
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[0].slug}"]`)).toBeVisible();
|
||||
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[1].slug}"]`)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("cannot book same slot multiple times", async ({ page }) => {
|
||||
@@ -58,32 +57,24 @@ test.describe("free user", () => {
|
||||
await expect(page.locator("[data-testid=booking-fail]")).toBeVisible();
|
||||
});
|
||||
|
||||
// Why do we need this test. The previous test is testing /30min booking only ?
|
||||
todo("`/free/30min` is bookable");
|
||||
|
||||
test("`/free/60min` is not bookable", async ({ page }) => {
|
||||
test("Second event type is not bookable", async ({ page, users }) => {
|
||||
const [free] = users.get();
|
||||
// Not available in listing
|
||||
await expect(page.locator('[href="/free/60min"]')).toHaveCount(0);
|
||||
await expect(page.locator(`[href="/${free.username}/${free.eventTypes[1].slug}"]`)).toHaveCount(0);
|
||||
|
||||
await page.goto("/free/60min");
|
||||
await page.goto(`/${free.username}/${free.eventTypes[1].slug}`);
|
||||
// Not available on a direct visit to event type page
|
||||
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("pro user", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await page.goto(`/${pro.username}`);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/pro");
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
||||
@@ -96,9 +87,12 @@ test.describe("pro user", () => {
|
||||
await bookFirstEvent(page);
|
||||
});
|
||||
|
||||
test("can reschedule a booking", async ({ page }) => {
|
||||
await bookFirstEvent(page);
|
||||
test("can reschedule a booking", async ({ page, users, bookings }) => {
|
||||
const [pro] = users.get();
|
||||
const [eventType] = pro.eventTypes;
|
||||
await bookings.create(pro.id, pro.username, eventType.id);
|
||||
|
||||
await pro.login();
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="reschedule"]').nth(0).click();
|
||||
await page.locator('[data-testid="edit"]').click();
|
||||
@@ -118,9 +112,12 @@ test.describe("pro user", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Can cancel the recently created booking and rebook the same timeslot", async ({ page }) => {
|
||||
test("Can cancel the recently created booking and rebook the same timeslot", async ({ page, users }) => {
|
||||
await bookFirstEvent(page);
|
||||
|
||||
const [pro] = users.get();
|
||||
await pro.login();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="cancel"]').first().click();
|
||||
await page.waitForNavigation({
|
||||
@@ -135,7 +132,7 @@ test.describe("pro user", () => {
|
||||
return url.pathname === "/cancel/success";
|
||||
},
|
||||
});
|
||||
await page.goto("/pro");
|
||||
await page.goto(`/${pro.username}`);
|
||||
await bookFirstEvent(page);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { deleteAllBookingsByEmail } from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
bookFirstEvent,
|
||||
bookTimeSlot,
|
||||
@@ -11,18 +11,15 @@ import {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("dynamic booking", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
await deleteAllBookingsByEmail("free@example.com");
|
||||
await page.goto("/pro+free");
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.login();
|
||||
const free = await users.create({ plan: "FREE" });
|
||||
await page.goto(`/${pro.username}+${free.username}`);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// delete test bookings
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
await deleteAllBookingsByEmail("free@example.com");
|
||||
test.afterEach(async ({ page, users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("book an event first day in next month", async ({ page }) => {
|
||||
|
||||
@@ -5,6 +5,8 @@ function chooseEmbedType(page: Page, embedType: string) {
|
||||
}
|
||||
|
||||
async function gotToPreviewTab(page: Page) {
|
||||
// To prevent early timeouts
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator("[data-testid=embed-tabs]").locator("text=Preview").click();
|
||||
}
|
||||
|
||||
@@ -179,7 +181,7 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
embedType: "inline",
|
||||
});
|
||||
|
||||
gotToPreviewTab(page);
|
||||
await gotToPreviewTab(page);
|
||||
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
|
||||
88
apps/web/playwright/fixtures/bookings.ts
Normal file
88
apps/web/playwright/fixtures/bookings.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Booking, Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5, v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const translator = short();
|
||||
|
||||
type BookingFixture = ReturnType<typeof createBookingFixture>;
|
||||
|
||||
// creates a user fixture instance and stores the collection
|
||||
export const createBookingsFixture = (page: Page) => {
|
||||
let store = { bookings: [], page } as { bookings: BookingFixture[]; page: typeof page };
|
||||
return {
|
||||
create: async (
|
||||
userId: number,
|
||||
username: string | null,
|
||||
eventTypeId = -1,
|
||||
{
|
||||
confirmed = true,
|
||||
rescheduled = false,
|
||||
paid = false,
|
||||
status = "ACCEPTED",
|
||||
}: Partial<Prisma.BookingCreateInput> = {}
|
||||
) => {
|
||||
const startDate = dayjs().add(1, "day").toDate();
|
||||
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: uid,
|
||||
title: "30min",
|
||||
startTime: startDate,
|
||||
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "attendee@example.com",
|
||||
name: "Attendee Example",
|
||||
timeZone: "Europe/London",
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
confirmed,
|
||||
rescheduled,
|
||||
paid,
|
||||
status,
|
||||
},
|
||||
});
|
||||
const bookingFixture = createBookingFixture(booking, store.page!);
|
||||
store.bookings.push(bookingFixture);
|
||||
return bookingFixture;
|
||||
},
|
||||
get: () => store.bookings,
|
||||
delete: async (id: number) => {
|
||||
await prisma.booking.delete({
|
||||
where: { id },
|
||||
});
|
||||
store.bookings = store.bookings.filter((b) => b.id !== id);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// creates the single user fixture
|
||||
const createBookingFixture = (booking: Booking, page: Page) => {
|
||||
const store = { booking, page };
|
||||
|
||||
// self is a reflective method that return the Prisma object that references this fixture.
|
||||
return {
|
||||
id: store.booking.id,
|
||||
uid: store.booking.uid,
|
||||
self: async () => (await prisma.booking.findUnique({ where: { id: store.booking.id } }))!,
|
||||
delete: async () => (await prisma.booking.delete({ where: { id: store.booking.id } }))!,
|
||||
};
|
||||
};
|
||||
59
apps/web/playwright/fixtures/payments.ts
Normal file
59
apps/web/playwright/fixtures/payments.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Payment } from "@prisma/client";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
type PaymentFixture = ReturnType<typeof createPaymentFixture>;
|
||||
|
||||
// creates a user fixture instance and stores the collection
|
||||
export const createPaymentsFixture = (page: Page) => {
|
||||
let store = { payments: [], page } as { payments: PaymentFixture[]; page: typeof page };
|
||||
return {
|
||||
create: async (
|
||||
bookingId: number,
|
||||
{ success = false, refunded = false }: { success?: boolean; refunded?: boolean } = {}
|
||||
) => {
|
||||
const payment = await prisma.payment.create({
|
||||
data: {
|
||||
uid: uuidv4(),
|
||||
amount: 20000,
|
||||
fee: 160,
|
||||
currency: "usd",
|
||||
success,
|
||||
refunded,
|
||||
type: "STRIPE",
|
||||
data: {},
|
||||
externalId: "DEMO_PAYMENT_FROM_DB_" + Date.now(),
|
||||
booking: {
|
||||
connect: {
|
||||
id: bookingId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const paymentFixture = createPaymentFixture(payment, store.page!);
|
||||
store.payments.push(paymentFixture);
|
||||
return paymentFixture;
|
||||
},
|
||||
get: () => store.payments,
|
||||
delete: async (id: number) => {
|
||||
await prisma.payment.delete({
|
||||
where: { id },
|
||||
});
|
||||
store.payments = store.payments.filter((b) => b.id !== id);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// creates the single user fixture
|
||||
const createPaymentFixture = (payment: Payment, page: Page) => {
|
||||
const store = { payment, page };
|
||||
|
||||
// self is a reflective method that return the Prisma object that references this fixture.
|
||||
return {
|
||||
id: store.payment.id,
|
||||
self: async () => (await prisma.payment.findUnique({ where: { id: store.payment.id } }))!,
|
||||
delete: async () => (await prisma.payment.delete({ where: { id: store.payment.id } }))!,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
import type { Page, WorkerInfo } from "@playwright/test";
|
||||
import type Prisma from "@prisma/client";
|
||||
import { Prisma as PrismaType, UserPlan } from "@prisma/client";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -9,13 +9,42 @@ import { TimeZoneEnum } from "./types";
|
||||
|
||||
type UserFixture = ReturnType<typeof createUserFixture>;
|
||||
|
||||
const userIncludes = PrismaType.validator<PrismaType.UserInclude>()({
|
||||
eventTypes: true,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
const userWithEventTypes = PrismaType.validator<PrismaType.UserArgs>()({
|
||||
include: userIncludes,
|
||||
});
|
||||
|
||||
type UserWithIncludes = PrismaType.UserGetPayload<typeof userWithEventTypes>;
|
||||
|
||||
// creates a user fixture instance and stores the collection
|
||||
export const createUsersFixture = (page: Page) => {
|
||||
export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
|
||||
let store = { users: [], page } as { users: UserFixture[]; page: typeof page };
|
||||
return {
|
||||
create: async (opts?: CustomUserOpts) => {
|
||||
const user = await prisma.user.create({
|
||||
data: await createUser(opts),
|
||||
const _user = await prisma.user.create({
|
||||
data: await createUser(workerInfo, opts),
|
||||
});
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
users: {
|
||||
connect: {
|
||||
id: _user.id,
|
||||
},
|
||||
},
|
||||
title: "Paid",
|
||||
slug: "paid",
|
||||
length: 30,
|
||||
price: 1000,
|
||||
},
|
||||
});
|
||||
const user = await prisma.user.findUnique({
|
||||
rejectOnNotFound: true,
|
||||
where: { id: _user.id },
|
||||
include: userIncludes,
|
||||
});
|
||||
const userFixture = createUserFixture(user, store.page!);
|
||||
store.users.push(userFixture);
|
||||
@@ -25,25 +54,39 @@ export const createUsersFixture = (page: Page) => {
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
},
|
||||
deleteAll: async () => {
|
||||
const ids = store.users.map((u) => u.id);
|
||||
await prisma.user.deleteMany({ where: { id: { in: ids } } });
|
||||
store.users = [];
|
||||
},
|
||||
delete: async (id: number) => {
|
||||
await prisma.user.delete({ where: { id } });
|
||||
store.users = store.users.filter((b) => b.id !== id);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type JSONValue = string | number | boolean | { [x: string]: JSONValue } | Array<JSONValue>;
|
||||
|
||||
// creates the single user fixture
|
||||
const createUserFixture = (user: Prisma.User, page: Page) => {
|
||||
const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
const store = { user, page };
|
||||
|
||||
// self is a reflective method that return the Prisma object that references this fixture.
|
||||
const self = async () => (await prisma.user.findUnique({ where: { id: store.user.id } }))!;
|
||||
const self = async () =>
|
||||
(await prisma.user.findUnique({ where: { id: store.user.id }, include: { eventTypes: true } }))!;
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
eventTypes: user.eventTypes!,
|
||||
self,
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
getPaymentCredential: async () => getPaymentCredential(store.page),
|
||||
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
|
||||
debug: async (message: string | Record<string, JSONValue>) => {
|
||||
await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } });
|
||||
},
|
||||
delete: async () => (await prisma.user.delete({ where: { id: store.user.id } }))!,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,10 +94,14 @@ type CustomUserOptsKeys = "username" | "password" | "plan" | "completedOnboardin
|
||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & { timeZone?: TimeZoneEnum };
|
||||
|
||||
// creates the actual user in the db.
|
||||
const createUser = async (opts?: CustomUserOpts) => {
|
||||
const createUser = async (
|
||||
workerInfo: WorkerInfo,
|
||||
opts?: CustomUserOpts
|
||||
): Promise<PrismaType.UserCreateInput> => {
|
||||
// build a unique name for our user
|
||||
const uname =
|
||||
(opts?.username ?? opts?.plan?.toLocaleLowerCase() ?? UserPlan.PRO.toLowerCase()) + "-" + Date.now();
|
||||
const uname = `${opts?.username ?? opts?.plan?.toLocaleLowerCase() ?? UserPlan.PRO.toLowerCase()}-${
|
||||
workerInfo.workerIndex
|
||||
}-${Date.now()}`;
|
||||
return {
|
||||
username: uname,
|
||||
name: (opts?.username ?? opts?.plan ?? UserPlan.PRO).toUpperCase(),
|
||||
@@ -65,6 +112,13 @@ const createUser = async (opts?: CustomUserOpts) => {
|
||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||
locale: opts?.locale ?? "en",
|
||||
eventTypes: {
|
||||
create: {
|
||||
title: "30 min",
|
||||
slug: "30-min",
|
||||
length: 30,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -74,10 +128,10 @@ export async function login(
|
||||
page: Page
|
||||
) {
|
||||
// get locators
|
||||
const loginLocator = await page.locator("[data-testid=login-form]");
|
||||
const emailLocator = await loginLocator.locator("#email");
|
||||
const passwordLocator = await loginLocator.locator("#password");
|
||||
const signInLocator = await loginLocator.locator('[type="submit"]');
|
||||
const loginLocator = page.locator("[data-testid=login-form]");
|
||||
const emailLocator = loginLocator.locator("#email");
|
||||
const passwordLocator = loginLocator.locator("#password");
|
||||
const signInLocator = loginLocator.locator('[type="submit"]');
|
||||
|
||||
//login
|
||||
await page.goto("/");
|
||||
@@ -88,3 +142,19 @@ export async function login(
|
||||
// 2 seconds of delay to give the session enough time for a clean load
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
export async function getPaymentCredential(page: Page) {
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
/** We start the Stripe flow */
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "https://connect.stripe.com/oauth/v2/authorize?*" }),
|
||||
page.click('li:has-text("Stripe") >> [data-testid="integration-connection-button"]'),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "/apps/installed" }),
|
||||
/** We skip filling Stripe forms (testing mode only) */
|
||||
page.click('[id="skip-account-app"]'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { deleteAllBookingsByEmail } from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("hash my url", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
let $url = "";
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
test.beforeEach(async ({ users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// delete test bookings
|
||||
await deleteAllBookingsByEmail("pro@example.com");
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("generate url hash", async ({ page }) => {
|
||||
// await page.pause();
|
||||
await page.goto("/event-types");
|
||||
@@ -34,22 +29,22 @@ test.describe("hash my url", () => {
|
||||
!isChecked && (await page.click('//*[@id="hashedLinkCheck"]'));
|
||||
// we wait for the hashedLink setting to load
|
||||
await page.waitForSelector('//*[@data-testid="generated-hash-url"]');
|
||||
$url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
|
||||
const $url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
|
||||
// click update
|
||||
await page.focus('//button[@type="submit"]');
|
||||
await page.keyboard.press("Enter");
|
||||
});
|
||||
|
||||
test("book using generated url hash", async ({ page }) => {
|
||||
// To prevent an early 404
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// book using generated url hash
|
||||
await page.goto($url);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test("hash regenerates after successful booking", async ({ page }) => {
|
||||
// hash regenerates after successful booking
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import * as teardown from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { selectFirstAvailableTimeSlotNextMonth, todo } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
const IS_STRIPE_ENABLED = !!(
|
||||
process.env.STRIPE_CLIENT_ID &&
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_PRIVATE_KEY
|
||||
);
|
||||
|
||||
test.describe.serial("Stripe integration", () => {
|
||||
test.afterAll(() => {
|
||||
teardown.deleteAllPaymentsByEmail("pro@example.com");
|
||||
teardown.deleteAllBookingsByEmail("pro@example.com");
|
||||
teardown.deleteAllPaymentCredentialsByEmail("pro@example.com");
|
||||
});
|
||||
test.describe("Stripe integration", () => {
|
||||
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
|
||||
|
||||
test.describe.serial("Stripe integration dashboard", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test("Can add Stripe integration", async ({ page }) => {
|
||||
test.describe("Stripe integration dashboard", () => {
|
||||
test("Can add Stripe integration", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await page.goto("/apps/installed");
|
||||
/** We should see the "Connect" button for Stripe */
|
||||
await expect(
|
||||
page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`)
|
||||
).toContainText("Connect");
|
||||
|
||||
/** We start the Stripe flow */
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "https://connect.stripe.com/oauth/v2/authorize?*" }),
|
||||
page.click('li:has-text("Stripe") >> [data-testid="integration-connection-button"]'),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "/apps/installed" }),
|
||||
/** We skip filling Stripe forms (testing mode only) */
|
||||
page.click('[id="skip-account-app"]'),
|
||||
]);
|
||||
await user.getPaymentCredential();
|
||||
|
||||
/** If Stripe is added correctly we should see the "Disconnect" button */
|
||||
await expect(
|
||||
page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`)
|
||||
).toContainText("Disconnect");
|
||||
|
||||
// Cleanup
|
||||
await user.delete();
|
||||
});
|
||||
});
|
||||
|
||||
test("Can book a paid booking", async ({ page }) => {
|
||||
await page.goto("/pro/paid");
|
||||
test("Can book a paid booking", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid")!;
|
||||
await user.login();
|
||||
await page.goto("/apps/installed");
|
||||
await user.getPaymentCredential();
|
||||
|
||||
await page.goto(`${user.username}/${eventType.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', "Stripe Stripeson");
|
||||
@@ -72,6 +64,9 @@ test.describe.serial("Stripe integration", () => {
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// Cleanup
|
||||
await user.delete();
|
||||
});
|
||||
|
||||
todo("Pending payment booking should not be confirmed by default");
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import * as teardown from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth, todo, waitFor } from "./lib/testUtils";
|
||||
|
||||
test.describe("integrations", () => {
|
||||
//teardown
|
||||
test.afterAll(async () => {
|
||||
await teardown.deleteAllWebhooksByEmail("pro@example.com");
|
||||
await teardown.deleteAllBookingsByEmail("pro@example.com");
|
||||
});
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/apps/installed");
|
||||
});
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Integrations", () => {
|
||||
todo("Can add Zoom integration");
|
||||
|
||||
todo("Can add Google Calendar");
|
||||
@@ -25,8 +16,15 @@ test.describe("integrations", () => {
|
||||
|
||||
todo("Can add Apple Calendar");
|
||||
|
||||
test("add webhook & test that creating an event triggers a webhook call", async ({ page }, testInfo) => {
|
||||
test("add webhook & test that creating an event triggers a webhook call", async ({
|
||||
page,
|
||||
users,
|
||||
}, testInfo) => {
|
||||
const webhookReceiver = createHttpServer();
|
||||
const user = await users.create();
|
||||
const [eventType] = user.eventTypes;
|
||||
await user.login();
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
// --- add webhook
|
||||
await page.click('[data-testid="new_webhook"]');
|
||||
@@ -43,7 +41,7 @@ test.describe("integrations", () => {
|
||||
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||
|
||||
// --- Book the first available day next month in the pro user's "30min"-event
|
||||
await page.goto(`/pro/30min`);
|
||||
await page.goto(`/${user.username}/${eventType.slug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
// --- fill form
|
||||
@@ -69,6 +67,7 @@ test.describe("integrations", () => {
|
||||
attendee.timeZone = dynamic;
|
||||
attendee.language = dynamic;
|
||||
}
|
||||
body.payload.organizer.email = dynamic;
|
||||
body.payload.organizer.timeZone = dynamic;
|
||||
body.payload.organizer.language = dynamic;
|
||||
body.payload.uid = dynamic;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":"","additionalNotes":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
|
||||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Booking } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5, v4 as uuidv4 } from "uuid";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const translator = short();
|
||||
|
||||
const TestUtilCreateBookingOnUserId = async (
|
||||
userId: number,
|
||||
username: string,
|
||||
eventTypeId: number,
|
||||
{ confirmed = true, rescheduled = false, paid = false, status = "ACCEPTED" }: Partial<Booking>
|
||||
) => {
|
||||
const startDate = dayjs().add(1, "day").toDate();
|
||||
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}`;
|
||||
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
||||
return await prisma?.booking.create({
|
||||
data: {
|
||||
uid: uid,
|
||||
title: "30min",
|
||||
startTime: startDate,
|
||||
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "attendee@example.com",
|
||||
name: "Attendee Example",
|
||||
timeZone: "Europe/London",
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
confirmed,
|
||||
rescheduled,
|
||||
paid,
|
||||
status,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const TestUtilCreatePayment = async (
|
||||
bookingId: number,
|
||||
{ success = false, refunded = false }: { success?: boolean; refunded?: boolean }
|
||||
) => {
|
||||
return await prisma?.payment.create({
|
||||
data: {
|
||||
uid: uuidv4(),
|
||||
amount: 20000,
|
||||
fee: 160,
|
||||
currency: "usd",
|
||||
success,
|
||||
refunded,
|
||||
type: "STRIPE",
|
||||
data: {},
|
||||
externalId: "DEMO_PAYMENT_FROM_DB",
|
||||
booking: {
|
||||
connect: {
|
||||
id: bookingId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { TestUtilCreateBookingOnUserId, TestUtilCreatePayment };
|
||||
@@ -1,14 +1,29 @@
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
import { createBookingsFixture } from "../fixtures/bookings";
|
||||
import { createPaymentsFixture } from "../fixtures/payments";
|
||||
import { createUsersFixture } from "../fixtures/users";
|
||||
|
||||
interface Fixtures {
|
||||
users: ReturnType<typeof createUsersFixture>;
|
||||
bookings: ReturnType<typeof createBookingsFixture>;
|
||||
payments: ReturnType<typeof createPaymentsFixture>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-fixtures
|
||||
*/
|
||||
export const test = base.extend<Fixtures>({
|
||||
users: async ({ page }, use) => {
|
||||
const usersFixture = createUsersFixture(page);
|
||||
users: async ({ page }, use, workerInfo) => {
|
||||
const usersFixture = createUsersFixture(page, workerInfo);
|
||||
await use(usersFixture);
|
||||
},
|
||||
bookings: async ({ page }, use) => {
|
||||
const bookingsFixture = createBookingsFixture(page);
|
||||
await use(bookingsFixture);
|
||||
},
|
||||
payments: async ({ page }, use) => {
|
||||
const payemntsFixture = createPaymentsFixture(page);
|
||||
await use(payemntsFixture);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,14 @@ import { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* DO NOT USE, since test run in parallel this will cause flaky tests. The reason
|
||||
* being that a set of test may end earlier than other trigger a delete of all bookings
|
||||
* than other tests may depend on them. The proper ettiquete should be that EACH test
|
||||
* should cleanup ONLY the booking that we're created in that specific test to se DB
|
||||
* remains "pristine" after each test
|
||||
*/
|
||||
export const deleteAllBookingsByEmail = async (
|
||||
email: string,
|
||||
whereConditional: Prisma.BookingWhereInput = {}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
|
||||
import { login } from "./fixtures/users";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { localize } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Login and logout tests", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
// Test login with all plans
|
||||
const plans = [UserPlan.PRO, UserPlan.FREE, UserPlan.TRIAL];
|
||||
plans.forEach((plan) => {
|
||||
test(`Should login with a ${plan} account`, async ({ page, users }) => {
|
||||
// Create user and login
|
||||
const pro = await users.create({ plan });
|
||||
await pro.login();
|
||||
const user = await users.create({ plan });
|
||||
await user.login();
|
||||
|
||||
const shellLocator = await page.locator(`[data-testid=dashboard-shell]`);
|
||||
const shellLocator = page.locator(`[data-testid=dashboard-shell]`);
|
||||
|
||||
// expects the home page for an authorized user
|
||||
await page.goto("/");
|
||||
@@ -25,7 +28,7 @@ test.describe("Login and logout tests", () => {
|
||||
// Asserts to read the tested plan
|
||||
const planLocator = shellLocator.locator(`[data-testid=plan-${plan.toLowerCase()}]`);
|
||||
await expect(planLocator).toBeVisible();
|
||||
await await expect(planLocator).toHaveText;
|
||||
await expect(planLocator).toHaveText(`-${plan}`);
|
||||
|
||||
// When TRIAL check if the TRIAL banner is visible
|
||||
if (plan === UserPlan.TRIAL) {
|
||||
@@ -34,7 +37,7 @@ test.describe("Login and logout tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Should warn when user does not exist", async ({ page, users }) => {
|
||||
test("Should warn when user does not exist", async ({ page }) => {
|
||||
const alertMessage = (await localize("en"))("no_account_exists");
|
||||
|
||||
// Login with a non-existent user
|
||||
@@ -51,7 +54,7 @@ test.describe("Login and logout tests", () => {
|
||||
const pro = await users.create({ username: "pro" });
|
||||
|
||||
// login with a wrong password
|
||||
await login({ username: "pro", password: "wrong" }, page);
|
||||
await login({ username: pro.username, password: "wrong" }, page);
|
||||
|
||||
// assert for the visibility of the localized alert message
|
||||
await expect(page.locator(`text=${alertMessage}`)).toBeVisible();
|
||||
@@ -59,8 +62,7 @@ test.describe("Login and logout tests", () => {
|
||||
|
||||
test("Should logout", async ({ page, users }) => {
|
||||
const signOutLabel = (await localize("en"))("sign_out");
|
||||
const userDropdownDisclose = async () =>
|
||||
(await page.locator("[data-testid=user-dropdown-trigger]")).click();
|
||||
const userDropdownDisclose = async () => page.locator("[data-testid=user-dropdown-trigger]").click();
|
||||
|
||||
// creates a user and login
|
||||
const pro = await users.create();
|
||||
@@ -68,7 +70,7 @@ test.describe("Login and logout tests", () => {
|
||||
|
||||
// disclose and click the sign out button from the user dropdown
|
||||
await userDropdownDisclose();
|
||||
const signOutBtn = await page.locator(`text=${signOutLabel}`);
|
||||
const signOutBtn = page.locator(`text=${signOutLabel}`);
|
||||
await signOutBtn.click();
|
||||
|
||||
// 2s of delay to assure the session is cleared
|
||||
|
||||
@@ -30,6 +30,11 @@ test.describe("Onboarding", () => {
|
||||
|
||||
test.describe("Onboarding", () => {
|
||||
test("update onboarding username via localstorage", async ({ page }) => {
|
||||
/**
|
||||
* We need to come up with a better test since all test are run in an incognito window.
|
||||
* Meaning that all localstorage access is null here.
|
||||
*/
|
||||
test.fixme();
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("username", "alwaysavailable");
|
||||
}, {});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { BookingStatus } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { TestUtilCreateBookingOnUserId, TestUtilCreatePayment } from "./lib/dbSetup";
|
||||
import { deleteAllBookingsByEmail } from "./lib/teardown";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
const IS_STRIPE_ENABLED = !!(
|
||||
@@ -13,51 +11,21 @@ const IS_STRIPE_ENABLED = !!(
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_PRIVATE_KEY
|
||||
);
|
||||
const findUserByEmail = async (email: string) => {
|
||||
return await prisma.user.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
credentials: true,
|
||||
},
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Reschedule Tests", async () => {
|
||||
let currentUser: Awaited<ReturnType<typeof findUserByEmail>>;
|
||||
// Using logged in state from globalSetup
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
test.beforeAll(async () => {
|
||||
currentUser = await findUserByEmail("pro@example.com");
|
||||
});
|
||||
test.afterEach(async () => {
|
||||
try {
|
||||
await deleteAllBookingsByEmail("pro@example.com", {
|
||||
createdAt: { gte: dayjs().startOf("day").toISOString() },
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while trying to delete all bookings from pro user");
|
||||
}
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
test("Should do a booking request reschedule from /bookings", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
|
||||
test("Should do a booking request reschedule from /bookings", async ({ page }) => {
|
||||
const user = currentUser;
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
slug: "30min",
|
||||
},
|
||||
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id!, {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
let originalBooking;
|
||||
if (user && user.id && user.username && eventType) {
|
||||
originalBooking = await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
}
|
||||
|
||||
await user.login();
|
||||
await page.goto("/bookings/upcoming");
|
||||
|
||||
await page.locator('[data-testid="reschedule"]').nth(0).click();
|
||||
@@ -67,176 +35,119 @@ test.describe("Reschedule Tests", async () => {
|
||||
await page.fill('[data-testid="reschedule_reason"]', "I can't longer have it");
|
||||
|
||||
await page.locator('button[data-testid="send_request"]').click();
|
||||
await expect(page.locator('[id="modal-title"]')).not.toBeVisible();
|
||||
|
||||
await page.goto("/bookings/cancelled");
|
||||
const updatedBooking = await booking.self();
|
||||
|
||||
// Find booking that was recently cancelled
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
cancellationReason: true,
|
||||
status: true,
|
||||
rescheduled: true,
|
||||
},
|
||||
where: { id: originalBooking?.id },
|
||||
});
|
||||
|
||||
expect(booking?.rescheduled).toBe(true);
|
||||
expect(booking?.cancellationReason).toBe("I can't longer have it");
|
||||
expect(booking?.status).toBe(BookingStatus.CANCELLED);
|
||||
expect(updatedBooking.rescheduled).toBe(true);
|
||||
expect(updatedBooking.cancellationReason).toBe("I can't longer have it");
|
||||
expect(updatedBooking.status).toBe(BookingStatus.CANCELLED);
|
||||
await booking.delete();
|
||||
});
|
||||
|
||||
test("Should display former time when rescheduling availability", async ({ page }) => {
|
||||
const user = currentUser;
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
slug: "30min",
|
||||
},
|
||||
test("Should display former time when rescheduling availability", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id!, {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
});
|
||||
let originalBooking;
|
||||
if (user && user.id && user.username && eventType) {
|
||||
originalBooking = await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
});
|
||||
}
|
||||
|
||||
await page.goto(
|
||||
`/${originalBooking?.user?.username}/${eventType?.slug}?rescheduleUid=${originalBooking?.uid}`
|
||||
);
|
||||
const formerTimeElement = await page.locator('[data-testid="former_time_p_desktop"]');
|
||||
await page.goto(`/${user.username}/${user.eventTypes[0].slug}?rescheduleUid=${booking.uid}`);
|
||||
const formerTimeElement = page.locator('[data-testid="former_time_p_desktop"]');
|
||||
await expect(formerTimeElement).toBeVisible();
|
||||
await booking.delete();
|
||||
});
|
||||
|
||||
test("Should display request reschedule send on bookings/cancelled", async ({ page }) => {
|
||||
const user = currentUser;
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
slug: "30min",
|
||||
},
|
||||
test("Should display request reschedule send on bookings/cancelled", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id, {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
});
|
||||
|
||||
if (user && user.id && user.username && eventType) {
|
||||
await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
});
|
||||
}
|
||||
await user.login();
|
||||
await page.goto("/bookings/cancelled");
|
||||
|
||||
const requestRescheduleSentElement = await page.locator('[data-testid="request_reschedule_sent"]').nth(1);
|
||||
const requestRescheduleSentElement = page.locator('[data-testid="request_reschedule_sent"]').nth(1);
|
||||
await expect(requestRescheduleSentElement).toBeVisible();
|
||||
await booking.delete();
|
||||
});
|
||||
|
||||
test("Should do a reschedule from user owner", async ({ page }) => {
|
||||
const user = currentUser;
|
||||
test("Should do a reschedule from user owner", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const [eventType] = user.eventTypes;
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
});
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await expect(page.locator('[name="name"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="email"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="notes"]')).toBeDisabled();
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// NOTE: remove if old booking should not be deleted
|
||||
expect(await booking.self()).toBeNull();
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
||||
expect(newBooking).not.toBeNull();
|
||||
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
||||
});
|
||||
|
||||
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
|
||||
test.skip(!IS_STRIPE_ENABLED, "Skipped as Stripe is not installed");
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.getPaymentCredential();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid")!;
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
rescheduled: true,
|
||||
status: BookingStatus.CANCELLED,
|
||||
paid: false,
|
||||
});
|
||||
|
||||
const payment = await payments.create(booking.id);
|
||||
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.indexOf("/payment") > -1;
|
||||
},
|
||||
});
|
||||
if (user?.id && user?.username && eventType?.id) {
|
||||
const booking = await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
rescheduled: true,
|
||||
status: BookingStatus.CANCELLED,
|
||||
});
|
||||
|
||||
await page.goto(`/${user?.username}/${eventType?.slug}?rescheduleUid=${booking?.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await expect(page.locator('[name="name"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="email"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="notes"]')).toBeDisabled();
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// NOTE: remove if old booking should not be deleted
|
||||
const oldBooking = await prisma.booking.findFirst({ where: { id: booking?.id } });
|
||||
expect(oldBooking).toBeNull();
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
|
||||
expect(newBooking).not.toBeNull();
|
||||
}
|
||||
await expect(page).toHaveURL(/.*payment/);
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
test("Unpaid rescheduling should go to payment page", async ({ page }) => {
|
||||
let user = currentUser;
|
||||
|
||||
test.skip(
|
||||
IS_STRIPE_ENABLED && !(user && user.credentials.length > 0),
|
||||
"Skipped as stripe is not installed and user is missing credentials"
|
||||
);
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
slug: "paid",
|
||||
},
|
||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid")!;
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
rescheduled: true,
|
||||
status: BookingStatus.CANCELLED,
|
||||
paid: true,
|
||||
});
|
||||
if (user?.id && user?.username && eventType?.id) {
|
||||
const booking = await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
rescheduled: true,
|
||||
status: BookingStatus.CANCELLED,
|
||||
paid: false,
|
||||
});
|
||||
if (booking?.id) {
|
||||
await TestUtilCreatePayment(booking.id, {});
|
||||
await page.goto(`/${user?.username}/${eventType?.slug}?rescheduleUid=${booking?.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
const payment = await payments.create(booking.id);
|
||||
await page.goto(`/${user?.username}/${eventType?.slug}?rescheduleUid=${booking?.uid}`);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.indexOf("/payment") > -1;
|
||||
},
|
||||
});
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*payment/);
|
||||
}
|
||||
}
|
||||
});
|
||||
await expect(page).toHaveURL(/.*success/);
|
||||
|
||||
test("Paid rescheduling should go to success page", async ({ page }) => {
|
||||
let user = currentUser;
|
||||
try {
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
slug: "paid",
|
||||
},
|
||||
});
|
||||
if (user?.id && user?.username && eventType?.id) {
|
||||
const booking = await TestUtilCreateBookingOnUserId(user?.id, user?.username, eventType?.id, {
|
||||
rescheduled: true,
|
||||
status: BookingStatus.CANCELLED,
|
||||
paid: true,
|
||||
});
|
||||
if (booking?.id) {
|
||||
await TestUtilCreatePayment(booking.id, {});
|
||||
await page.goto(`/${user?.username}/${eventType?.slug}?rescheduleUid=${booking?.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*success/);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await prisma.payment.delete({
|
||||
where: {
|
||||
externalId: "DEMO_PAYMENT_FROM_DB",
|
||||
},
|
||||
});
|
||||
}
|
||||
await payment.delete();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,8 @@ addAliases({
|
||||
"@ee": __dirname + "/apps/web/ee",
|
||||
});
|
||||
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
const outputDir = path.join(__dirname, "..", "..", "test-results");
|
||||
const testDir = path.join(__dirname, "..", "..", "apps/web/playwright");
|
||||
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -2568,7 +2568,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
|
||||
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
|
||||
|
||||
"@prisma/client@^3.12.0", "@prisma/client@^3.13.0":
|
||||
"@prisma/client@^3.13.0":
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.13.0.tgz#84511ebdf6ba75f77ca08495b9f73f22c4255654"
|
||||
integrity sha512-lnEA2tTyVbO5mS1ehmHJQKBDiKB8shaR6s3azwj3Azfi5XHIfnqmkolLCvUeFYnkDCNVzGXJpUgKwQt/UOOYVQ==
|
||||
@@ -2589,11 +2589,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz#676aca309d66d9be2aad8911ca31f1ee5561041c"
|
||||
integrity sha512-TGp9rvgJIKo8NgvAHSwOosbut9mTA7VC6/rpQI9gh+ySSRjdQFhbGyNUiOcQrlI9Ob2DWeO7y4HEnhdKxYiECg==
|
||||
|
||||
"@prisma/engines@3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86":
|
||||
version "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86.tgz#2964113729a78b8b21e186b5592affd1fde73c16"
|
||||
integrity sha512-LjRssaWu9w2SrXitofnutRIyURI7l0veQYIALz7uY4shygM9nMcK3omXcObRm7TAcw3Z+9ytfK1B+ySOsOesxQ==
|
||||
|
||||
"@prisma/engines@3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b":
|
||||
version "3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.13.0-17.efdf9b1183dddfd4258cd181a72125755215ab7b.tgz#d3a457cec4ef7a3b3412c45b1f2eac68c974474b"
|
||||
@@ -13687,13 +13682,6 @@ prism-react-renderer@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"
|
||||
integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ==
|
||||
|
||||
prisma@3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.10.0.tgz#872d87afbeb1cbcaa77c3d6a63c125e0d704b04d"
|
||||
integrity sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA==
|
||||
dependencies:
|
||||
"@prisma/engines" "3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86"
|
||||
|
||||
prisma@^3.13.0:
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.13.0.tgz#b11edd5631222ff1bf1d5324732d47801386aa8c"
|
||||
|
||||
Reference in New Issue
Block a user