2023-10-10 03:10:04 +00:00
import type { Frame , Page } from "@playwright/test" ;
2023-03-01 20:18:51 +00:00
import { expect } from "@playwright/test" ;
2023-10-20 23:57:13 +00:00
import EventEmitter from "events" ;
2023-02-16 22:39:57 +00:00
import type { IncomingMessage , ServerResponse } from "http" ;
import { createServer } from "http" ;
2023-08-18 18:13:21 +00:00
// eslint-disable-next-line no-restricted-imports
2023-03-23 18:49:28 +00:00
import { noop } from "lodash" ;
2023-08-23 09:08:14 +00:00
import type { API , Messages } from "mailhog" ;
2021-10-18 21:07:06 +00:00
2023-09-07 16:27:46 +00:00
import type { Prisma } from "@calcom/prisma/client" ;
import { BookingStatus } from "@calcom/prisma/enums" ;
import type { Fixtures } from "./fixtures" ;
2023-03-01 20:18:51 +00:00
import { test } from "./fixtures" ;
2021-12-15 16:25:49 +00:00
export function todo ( title : string ) {
2022-05-18 16:25:30 +00:00
// eslint-disable-next-line playwright/no-skipped-test
test . skip ( title , noop ) ;
2021-12-15 16:25:49 +00:00
}
2021-10-18 21:07:06 +00:00
type Request = IncomingMessage & { body? : unknown } ;
type RequestHandlerOptions = { req : Request ; res : ServerResponse } ;
type RequestHandler = ( opts : RequestHandlerOptions ) = > void ;
2023-05-22 23:15:06 +00:00
export const testEmail = "test@example.com" ;
export const testName = "Test Testson" ;
2023-07-27 11:48:08 +00:00
export const teamEventTitle = "Team Event - 30min" ;
export const teamEventSlug = "team-event-30min" ;
2021-10-18 21:07:06 +00:00
export function createHttpServer ( opts : { requestHandler? : RequestHandler } = { } ) {
const {
requestHandler = ( { res } ) = > {
res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
res . write ( JSON . stringify ( { } ) ) ;
res . end ( ) ;
} ,
} = opts ;
2023-10-20 23:57:13 +00:00
const eventEmitter = new EventEmitter ( ) ;
2021-10-18 21:07:06 +00:00
const requestList : Request [ ] = [ ] ;
2023-10-20 23:57:13 +00:00
const waitForRequestCount = ( count : number ) = >
new Promise < void > ( ( resolve ) = > {
if ( requestList . length === count ) {
resolve ( ) ;
return ;
}
const pushHandler = ( ) = > {
if ( requestList . length !== count ) {
return ;
}
eventEmitter . off ( "push" , pushHandler ) ;
resolve ( ) ;
} ;
eventEmitter . on ( "push" , pushHandler ) ;
} ) ;
2021-10-18 21:07:06 +00:00
const server = createServer ( ( req , res ) = > {
const buffer : unknown [ ] = [ ] ;
req . on ( "data" , ( data ) = > {
buffer . push ( data ) ;
} ) ;
req . on ( "end" , ( ) = > {
const _req : Request = req ;
// assume all incoming request bodies are json
const json = buffer . length ? JSON . parse ( buffer . join ( "" ) ) : undefined ;
_req . body = json ;
requestList . push ( _req ) ;
2023-10-20 23:57:13 +00:00
eventEmitter . emit ( "push" ) ;
2021-10-18 21:07:06 +00:00
requestHandler ( { req : _req , res } ) ;
} ) ;
} ) ;
// listen on random port
server . listen ( 0 ) ;
2023-06-06 11:59:57 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2021-10-18 21:07:06 +00:00
const port : number = ( server . address ( ) as any ) . port ;
const url = ` http://localhost: ${ port } ` ;
2023-10-20 23:57:13 +00:00
2021-10-18 21:07:06 +00:00
return {
port ,
close : ( ) = > server . close ( ) ,
requestList ,
url ,
2023-10-20 23:57:13 +00:00
waitForRequestCount ,
2021-10-18 21:07:06 +00:00
} ;
}
2023-10-10 03:10:04 +00:00
export async function selectFirstAvailableTimeSlotNextMonth ( page : Page | Frame ) {
2022-05-31 09:32:41 +00:00
// Let current month dates fully render.
2022-03-08 22:40:31 +00:00
await page . click ( '[data-testid="incrementMonth"]' ) ;
2023-07-11 15:11:08 +00:00
2022-03-08 22:40:31 +00:00
// Waiting for full month increment
2023-07-11 15:11:08 +00:00
await page . locator ( '[data-testid="day"][data-disabled="false"]' ) . nth ( 0 ) . click ( ) ;
await page . locator ( '[data-testid="time"]' ) . nth ( 0 ) . click ( ) ;
2022-03-08 22:40:31 +00:00
}
2022-03-24 17:32:28 +00:00
export async function selectSecondAvailableTimeSlotNextMonth ( page : Page ) {
2022-05-31 09:32:41 +00:00
// Let current month dates fully render.
2022-03-24 17:32:28 +00:00
await page . click ( '[data-testid="incrementMonth"]' ) ;
2023-07-11 15:11:08 +00:00
2022-04-06 17:20:30 +00:00
await page . locator ( '[data-testid="day"][data-disabled="false"]' ) . nth ( 1 ) . click ( ) ;
2023-07-11 15:11:08 +00:00
await page . locator ( '[data-testid="time"]' ) . nth ( 0 ) . click ( ) ;
2022-03-24 17:32:28 +00:00
}
2022-04-06 15:13:09 +00:00
2022-10-27 09:34:34 +00:00
async function bookEventOnThisPage ( page : Page ) {
2022-04-06 17:20:30 +00:00
await selectFirstAvailableTimeSlotNextMonth ( page ) ;
2022-05-11 22:39:45 +00:00
await bookTimeSlot ( page ) ;
2022-04-06 17:20:30 +00:00
// Make sure we're navigated to the success page
2023-05-02 16:58:39 +00:00
await page . waitForURL ( ( url ) = > {
return url . pathname . startsWith ( "/booking" ) ;
2022-05-11 22:55:30 +00:00
} ) ;
2022-05-11 16:46:52 +00:00
await expect ( page . locator ( "[data-testid=success-page]" ) ) . toBeVisible ( ) ;
2022-04-06 17:20:30 +00:00
}
2022-10-27 09:34:34 +00:00
export async function bookOptinEvent ( page : Page ) {
await page . locator ( '[data-testid="event-type-link"]:has-text("Opt in")' ) . click ( ) ;
await bookEventOnThisPage ( page ) ;
}
export async function bookFirstEvent ( page : Page ) {
// Click first event type
await page . click ( '[data-testid="event-type-link"]' ) ;
await bookEventOnThisPage ( page ) ;
}
2023-03-14 04:19:05 +00:00
export const bookTimeSlot = async ( page : Page , opts ? : { name? : string ; email? : string } ) = > {
2022-04-06 17:20:30 +00:00
// --- fill form
2023-05-22 23:15:06 +00:00
await page . fill ( '[name="name"]' , opts ? . name ? ? testName ) ;
await page . fill ( '[name="email"]' , opts ? . email ? ? testEmail ) ;
2022-04-06 17:20:30 +00:00
await page . press ( '[name="email"]' , "Enter" ) ;
} ;
2022-04-06 15:13:09 +00:00
// Provide an standalone localize utility not managed by next-i18n
export async function localize ( locale : string ) {
const localeModule = ` ../../public/static/locales/ ${ locale } /common.json ` ;
const localeMap = await import ( localeModule ) ;
return ( message : string ) = > {
if ( message in localeMap ) return localeMap [ message ] ;
throw "No locale found for the given entry message" ;
} ;
}
2023-03-14 04:19:05 +00:00
export const createNewEventType = async ( page : Page , args : { eventTitle : string } ) = > {
await page . click ( "[data-testid=new-event-type]" ) ;
const eventTitle = args . eventTitle ;
await page . fill ( "[name=title]" , eventTitle ) ;
await page . fill ( "[name=length]" , "10" ) ;
await page . click ( "[type=submit]" ) ;
2023-05-02 16:58:39 +00:00
await page . waitForURL ( ( url ) = > {
return url . pathname !== "/event-types" ;
2023-03-14 04:19:05 +00:00
} ) ;
} ;
export const createNewSeatedEventType = async ( page : Page , args : { eventTitle : string } ) = > {
const eventTitle = args . eventTitle ;
await createNewEventType ( page , { eventTitle } ) ;
await page . locator ( '[data-testid="vertical-tab-event_advanced_tab_title"]' ) . click ( ) ;
await page . locator ( '[data-testid="offer-seats-toggle"]' ) . click ( ) ;
await page . locator ( '[data-testid="update-eventtype"]' ) . click ( ) ;
} ;
2023-06-13 15:22:19 +00:00
export async function gotoRoutingLink ( {
page ,
formId ,
queryString = "" ,
} : {
page : Page ;
formId? : string ;
queryString? : string ;
} ) {
let previewLink = null ;
if ( ! formId ) {
// Instead of clicking on the preview link, we are going to the preview link directly because the earlier opens a new tab which is a bit difficult to manage with Playwright
const href = await page . locator ( '[data-testid="form-action-preview"]' ) . getAttribute ( "href" ) ;
if ( ! href ) {
throw new Error ( "Preview link not found" ) ;
}
previewLink = href ;
} else {
previewLink = ` /forms/ ${ formId } ` ;
}
await page . goto ( ` ${ previewLink } ${ queryString ? ` ? ${ queryString } ` : "" } ` ) ;
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
2023-06-15 08:58:07 +00:00
await new Promise ( ( resolve ) = > setTimeout ( resolve , 1000 ) ) ;
2023-06-13 15:22:19 +00:00
}
2023-08-16 19:49:10 +00:00
export async function installAppleCalendar ( page : Page ) {
await page . goto ( "/apps/categories/calendar" ) ;
await page . click ( '[data-testid="app-store-app-card-apple-calendar"]' ) ;
await page . waitForURL ( "/apps/apple-calendar" ) ;
await page . click ( '[data-testid="install-app-button"]' ) ;
}
2023-09-07 16:27:46 +00:00
2023-08-23 09:08:14 +00:00
export async function getEmailsReceivedByUser ( {
emails ,
userEmail ,
} : {
emails? : API ;
userEmail : string ;
} ) : Promise < Messages | null > {
if ( ! emails ) return null ;
return emails . search ( userEmail , "to" ) ;
}
export async function expectEmailsToHaveSubject ( {
emails ,
organizer ,
booker ,
eventTitle ,
} : {
emails? : API ;
organizer : { name? : string | null ; email : string } ;
booker : { name : string ; email : string } ;
eventTitle : string ;
} ) {
if ( ! emails ) return null ;
const emailsOrganizerReceived = await getEmailsReceivedByUser ( { emails , userEmail : organizer.email } ) ;
const emailsBookerReceived = await getEmailsReceivedByUser ( { emails , userEmail : booker.email } ) ;
expect ( emailsOrganizerReceived ? . total ) . toBe ( 1 ) ;
expect ( emailsBookerReceived ? . total ) . toBe ( 1 ) ;
const [ organizerFirstEmail ] = ( emailsOrganizerReceived as Messages ) . items ;
const [ bookerFirstEmail ] = ( emailsBookerReceived as Messages ) . items ;
const emailSubject = ` ${ eventTitle } between ${ organizer . name ? ? "Nameless" } and ${ booker . name } ` ;
expect ( organizerFirstEmail . subject ) . toBe ( emailSubject ) ;
expect ( bookerFirstEmail . subject ) . toBe ( emailSubject ) ;
}
2023-09-07 16:27:46 +00:00
// this method is not used anywhere else
// but I'm keeping it here in case we need in the future
async function createUserWithSeatedEvent ( users : Fixtures [ "users" ] ) {
const slug = "seats" ;
const user = await users . create ( {
eventTypes : [
{
title : "Seated event" ,
slug ,
seatsPerTimeSlot : 10 ,
requiresConfirmation : true ,
length : 30 ,
disableGuests : true , // should always be true for seated events
} ,
] ,
} ) ;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eventType = user . eventTypes . find ( ( e ) = > e . slug === slug ) ! ;
return { user , eventType } ;
}
export async function createUserWithSeatedEventAndAttendees (
fixtures : Pick < Fixtures , " users " | " bookings " > ,
attendees : Prisma.AttendeeCreateManyBookingInput [ ]
) {
const { user , eventType } = await createUserWithSeatedEvent ( fixtures . users ) ;
const booking = await fixtures . bookings . create ( user . id , user . username , eventType . id , {
status : BookingStatus.ACCEPTED ,
// startTime with 1 day from now and endTime half hour after
startTime : new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 ) ,
endTime : new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 + 30 * 60 * 1000 ) ,
attendees : {
createMany : {
data : attendees ,
} ,
} ,
} ) ;
return { user , eventType , booking } ;
}