add e2e testing on webhooks and booking happy-path (#936)
This commit is contained in:
parent
86d292838c
commit
9e69029943
16 changed files with 271 additions and 80 deletions
|
@ -28,7 +28,9 @@
|
|||
"files": ["playwright/**/*.{js,jsx,tsx,ts}"],
|
||||
"rules": {
|
||||
"no-undef": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-implicit-any": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -90,7 +90,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
return (
|
||||
<div key={slot.time.format()}>
|
||||
<Link href={bookingUrl}>
|
||||
<a className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-primary-500 dark:border-transparent rounded-sm hover:text-white hover:bg-primary-500 dark:hover:border-black py-4 dark:hover:bg-black">
|
||||
<a
|
||||
className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-primary-500 dark:border-transparent rounded-sm hover:text-white hover:bg-primary-500 dark:hover:border-black py-4 dark:hover:bg-black"
|
||||
data-testid="time">
|
||||
{slot.time.format(timeFormat)}
|
||||
</a>
|
||||
</Link>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import dayjsBusinessDays from "dayjs-business-days";
|
||||
// Then, include dayjs-business-time
|
||||
import dayjsBusinessTime from "dayjs-business-time";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -9,11 +10,12 @@ import classNames from "@lib/classNames";
|
|||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getSlots from "@lib/slots";
|
||||
|
||||
dayjs.extend(dayjsBusinessDays);
|
||||
dayjs.extend(dayjsBusinessTime);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const DatePicker = ({
|
||||
// FIXME prop types
|
||||
function DatePicker({
|
||||
weekStart,
|
||||
onDatePicked,
|
||||
workingHours,
|
||||
|
@ -26,7 +28,7 @@ const DatePicker = ({
|
|||
periodDays,
|
||||
periodCountCalendarDays,
|
||||
minimumBookingNotice,
|
||||
}) => {
|
||||
}: any): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
|
||||
|
||||
|
@ -47,11 +49,11 @@ const DatePicker = ({
|
|||
|
||||
// Handle month changes
|
||||
const incrementMonth = () => {
|
||||
setSelectedMonth(selectedMonth + 1);
|
||||
setSelectedMonth((selectedMonth ?? 0) + 1);
|
||||
};
|
||||
|
||||
const decrementMonth = () => {
|
||||
setSelectedMonth(selectedMonth - 1);
|
||||
setSelectedMonth((selectedMonth ?? 0) - 1);
|
||||
};
|
||||
|
||||
const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth);
|
||||
|
@ -72,7 +74,7 @@ const DatePicker = ({
|
|||
case "rolling": {
|
||||
const periodRollingEndDay = periodCountCalendarDays
|
||||
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
|
||||
: dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day");
|
||||
: dayjs().tz(organizerTimeZone).addBusinessTime(periodDays, "days").endOf("day");
|
||||
return (
|
||||
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
|
||||
date.endOf("day").isAfter(periodRollingEndDay) ||
|
||||
|
@ -145,10 +147,13 @@ const DatePicker = ({
|
|||
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
|
||||
<button
|
||||
onClick={decrementMonth}
|
||||
className={
|
||||
"group mr-2 p-1" + (selectedMonth <= dayjs().month() && "text-gray-400 dark:text-gray-600")
|
||||
}
|
||||
disabled={selectedMonth <= dayjs().month()}>
|
||||
className={classNames(
|
||||
"group mr-2 p-1",
|
||||
typeof selectedMonth === "number" &&
|
||||
selectedMonth <= dayjs().month() &&
|
||||
"text-gray-400 dark:text-gray-600"
|
||||
)}
|
||||
disabled={typeof selectedMonth === "number" && selectedMonth <= dayjs().month()}>
|
||||
<ChevronLeftIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
|
||||
</button>
|
||||
<button className="group p-1" onClick={incrementMonth}>
|
||||
|
@ -190,7 +195,9 @@ const DatePicker = ({
|
|||
: !day.disabled
|
||||
? " bg-gray-100 dark:bg-gray-600"
|
||||
: ""
|
||||
)}>
|
||||
)}
|
||||
data-testid="day"
|
||||
data-disabled={day.disabled}>
|
||||
{day.date}
|
||||
</button>
|
||||
)}
|
||||
|
@ -199,6 +206,6 @@ const DatePicker = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default DatePicker;
|
||||
|
|
|
@ -5,7 +5,7 @@ const opts = {
|
|||
executablePath: process.env.PLAYWRIGHT_CHROME_EXECUTABLE_PATH,
|
||||
};
|
||||
|
||||
console.log("⚙️ Playwright options:", opts);
|
||||
console.log("⚙️ Playwright options:", JSON.stringify(opts, null, 4));
|
||||
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
transform: {
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
testMatch: ["<rootDir>/playwright/**/?(*.)+(spec|test).[jt]s?(x)"],
|
||||
testMatch: ["<rootDir>/playwright/**/*(*.)@(spec|test).[jt]s?(x)"],
|
||||
testEnvironmentOptions: {
|
||||
"jest-playwright": {
|
||||
browsers: ["chromium" /*, 'firefox', 'webkit'*/],
|
||||
|
|
|
@ -15,9 +15,8 @@ export function asNumberOrThrow(str: unknown) {
|
|||
}
|
||||
|
||||
export function asStringOrThrow(str: unknown): string {
|
||||
const type = typeof str;
|
||||
if (type !== "string") {
|
||||
throw new Error(`Expected "string" - got ${type}`);
|
||||
if (typeof str !== "string") {
|
||||
throw new Error(`Expected "string" - got ${typeof str}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
|
|
@ -8,21 +8,26 @@ const sendPayload = (
|
|||
): Promise<string | Response> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!subscriberUrl || !payload) {
|
||||
return reject("Missing required elements to send webhook payload.");
|
||||
return reject(new Error("Missing required elements to send webhook payload."));
|
||||
}
|
||||
const body = {
|
||||
triggerEvent: triggerEvent,
|
||||
createdAt: createdAt,
|
||||
payload: payload,
|
||||
};
|
||||
|
||||
fetch(subscriberUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
triggerEvent: triggerEvent,
|
||||
createdAt: createdAt,
|
||||
payload: payload,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
reject(new Error(`Response code ${response.status}`));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"test": "jest",
|
||||
"test-playwright": "jest --config jest.playwright.config.js",
|
||||
"test-playwright-lcov": "cross-env PLAYWRIGHT_HEADLESS=1 PLAYWRIGHT_COVERAGE=1 yarn test-playwright && nyc report --reporter=lcov",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
|
@ -58,7 +59,7 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dayjs": "^1.10.6",
|
||||
"dayjs-business-days": "^1.0.4",
|
||||
"dayjs-business-time": "^1.0.4",
|
||||
"googleapis": "^84.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"ical.js": "^1.4.0",
|
||||
|
@ -103,7 +104,7 @@
|
|||
"@types/jest": "^27.0.1",
|
||||
"@types/lodash": "^4.14.175",
|
||||
"@types/micro": "^7.3.6",
|
||||
"@types/node": "^16.6.1",
|
||||
"@types/node": "^16.10.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/qrcode": "^1.4.1",
|
||||
"@types/react": "^17.0.18",
|
||||
|
|
|
@ -25,7 +25,12 @@ type AugmentedNextPageContext = Omit<NextPageContext, "err"> & {
|
|||
|
||||
const log = logger.getChildLogger({ prefix: ["[error]"] });
|
||||
|
||||
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number } {
|
||||
export function getErrorFromUnknown(cause: unknown): Error & {
|
||||
// status code error
|
||||
statusCode?: number;
|
||||
// prisma error
|
||||
code?: unknown;
|
||||
} {
|
||||
if (cause instanceof Error) {
|
||||
return cause;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { SchedulingType, Prisma, Credential } from "@prisma/client";
|
||||
import { Credential, Prisma, SchedulingType } from "@prisma/client";
|
||||
import async from "async";
|
||||
import dayjs from "dayjs";
|
||||
import dayjsBusinessDays from "dayjs-business-days";
|
||||
import dayjsBusinessTime from "dayjs-business-time";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getErrorFromUnknown } from "pages/_error";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
|
@ -29,7 +30,7 @@ export interface DailyReturnType {
|
|||
created_at: string;
|
||||
}
|
||||
|
||||
dayjs.extend(dayjsBusinessDays);
|
||||
dayjs.extend(dayjsBusinessTime);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(timezone);
|
||||
|
@ -98,7 +99,7 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: string, length: number)
|
|||
|
||||
function isOutOfBounds(
|
||||
time: dayjs.ConfigType,
|
||||
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }
|
||||
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }: any // FIXME types
|
||||
): boolean {
|
||||
const date = dayjs(time);
|
||||
|
||||
|
@ -106,7 +107,7 @@ function isOutOfBounds(
|
|||
case "rolling": {
|
||||
const periodRollingEndDay = periodCountCalendarDays
|
||||
? dayjs().tz(timeZone).add(periodDays, "days").endOf("day")
|
||||
: dayjs().tz(timeZone).businessDaysAdd(periodDays, "days").endOf("day");
|
||||
: dayjs().tz(timeZone).addBusinessTime(periodDays, "days").endOf("day");
|
||||
return date.endOf("day").isAfter(periodRollingEndDay);
|
||||
}
|
||||
|
||||
|
@ -298,7 +299,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
startTime: dayjs(evt.startTime).toDate(),
|
||||
endTime: dayjs(evt.endTime).toDate(),
|
||||
description: evt.description,
|
||||
confirmed: !eventType.requiresConfirmation || !!rescheduleUid,
|
||||
confirmed: !eventType?.requiresConfirmation || !!rescheduleUid,
|
||||
location: evt.location,
|
||||
eventType: {
|
||||
connect: {
|
||||
|
@ -323,9 +324,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
let booking: Booking | null = null;
|
||||
try {
|
||||
booking = await createBooking();
|
||||
} catch (e) {
|
||||
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", e.message);
|
||||
if (e.code === "P2002") {
|
||||
} catch (_err) {
|
||||
const err = getErrorFromUnknown(_err);
|
||||
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
|
||||
if (err.code === "P2002") {
|
||||
res.status(409).json({ message: "booking.conflict" });
|
||||
return;
|
||||
}
|
||||
|
@ -361,7 +363,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
);
|
||||
|
||||
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter((time) => time);
|
||||
calendarBusyTimes.push(...videoBusyTimes);
|
||||
calendarBusyTimes.push(...(videoBusyTimes as any[])); // FIXME add types
|
||||
console.log("calendarBusyTimes==>>>", calendarBusyTimes);
|
||||
|
||||
const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/client";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
// GET /api/webhook/{hook}
|
||||
const webhooks = await prisma.webhook.findFirst({
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: {
|
||||
id: String(req.query.hook),
|
||||
userId: session.user.id,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
if (!webhook) {
|
||||
return res.status(404).json({ message: "Invalid Webhook" });
|
||||
}
|
||||
if (req.method === "GET") {
|
||||
return res.status(200).json({ webhooks: webhooks });
|
||||
return res.status(200).json({ webhook });
|
||||
}
|
||||
|
||||
// DELETE /api/webhook/{hook}
|
||||
|
@ -31,19 +35,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id: req.query.hook as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!webhook) {
|
||||
return res.status(404).json({ message: "Invalid Webhook" });
|
||||
}
|
||||
|
||||
await prisma.webhook.update({
|
||||
where: {
|
||||
id: req.query.hook as string,
|
||||
id: webhook.id,
|
||||
},
|
||||
data: {
|
||||
subscriberUrl: req.body.subscriberUrl,
|
||||
|
|
|
@ -138,6 +138,7 @@ function WebhookDialogForm(props: {
|
|||
});
|
||||
return (
|
||||
<Form
|
||||
data-testid="WebhookDialogForm"
|
||||
form={form}
|
||||
onSubmit={(event) => {
|
||||
form
|
||||
|
@ -248,7 +249,7 @@ function WebhookEmbed(props: { webhooks: TWebhook[] }) {
|
|||
<ListItemText component="p">Automation</ListItemText>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="secondary" onClick={() => setNewWebhookModal(true)}>
|
||||
<Button color="secondary" onClick={() => setNewWebhookModal(true)} data-testid="new_webhook">
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import timezone from "dayjs/plugin/timezone";
|
|||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { createEvent } from "ics";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -41,9 +42,9 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
const eventName = getEventName(name, props.eventType.title, props.eventType.eventName);
|
||||
|
||||
function eventLink(): string {
|
||||
const optional: { location?: string | string[] } = {};
|
||||
const optional: { location?: string } = {};
|
||||
if (location) {
|
||||
optional["location"] = location;
|
||||
optional["location"] = Array.isArray(location) ? location[0] : location;
|
||||
}
|
||||
|
||||
const event = createEvent({
|
||||
|
@ -51,7 +52,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
.utc()
|
||||
.toArray()
|
||||
.slice(0, 6)
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)),
|
||||
.map((v, i) => (i === 1 ? v + 1 : v)) as any, // <-- FIXME fix types, not sure what's going on here
|
||||
startInputType: "utc",
|
||||
title: eventName,
|
||||
description: props.eventType.description ? props.eventType.description : undefined,
|
||||
|
@ -71,7 +72,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
|
||||
return (
|
||||
(isReady && (
|
||||
<div className="h-screen bg-neutral-50 dark:bg-neutral-900">
|
||||
<div className="h-screen bg-neutral-50 dark:bg-neutral-900" data-testid="success-page">
|
||||
<HeadSeo
|
||||
title={needsConfirmation ? t("booking_submitted") : t("booking_confirmed")}
|
||||
description={needsConfirmation ? t("booking_submitted") : t("booking_confirmed")}
|
||||
|
@ -306,21 +307,22 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!eventType.users.length && eventType.userId) {
|
||||
eventType.users.push(
|
||||
await prisma.user.findUnique({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
theme: true,
|
||||
hideBranding: true,
|
||||
name: true,
|
||||
plan: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
// TODO we should add `user User` relation on `EventType` so this extra query isn't needed
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
hideBranding: true,
|
||||
plan: true,
|
||||
theme: true,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
eventType.users.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventType.users.length) {
|
||||
|
|
99
playwright/integrations.test.ts
Normal file
99
playwright/integrations.test.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import dayjs from "dayjs";
|
||||
import { kont } from "kont";
|
||||
|
||||
import { loginProvider } from "./lib/loginProvider";
|
||||
import { createHttpServer, waitFor } from "./lib/testUtils";
|
||||
|
||||
jest.setTimeout(60e3);
|
||||
jest.retryTimes(3);
|
||||
|
||||
describe("webhooks", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(
|
||||
loginProvider({
|
||||
user: "pro",
|
||||
path: "/integrations",
|
||||
waitForSelector: '[data-testid="new_webhook"]',
|
||||
})
|
||||
)
|
||||
.done();
|
||||
|
||||
test("add webhook & test that creating an event triggers a webhook call", async () => {
|
||||
const { page } = ctx;
|
||||
const webhookReceiver = createHttpServer();
|
||||
|
||||
// --- add webhook
|
||||
await page.click('[data-testid="new_webhook"]');
|
||||
await expect(page).toHaveSelector("[data-testid='WebhookDialogForm']");
|
||||
|
||||
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||
|
||||
await page.click("[type=submit]");
|
||||
|
||||
// dialog is closed
|
||||
await expect(page).not.toHaveSelector("[data-testid='WebhookDialogForm']");
|
||||
// page contains the url
|
||||
await expect(page).toHaveSelector(`text='${webhookReceiver.url}'`);
|
||||
|
||||
// --- go to tomorrow in the pro user's "30min"-event
|
||||
const tomorrow = dayjs().add(1, "day");
|
||||
const tomorrowFormatted = tomorrow.format("YYYY-MM-DDZZ");
|
||||
|
||||
await page.goto(`http://localhost:3000/pro/30min?date=${encodeURIComponent(tomorrowFormatted)}`);
|
||||
|
||||
// click first time available
|
||||
await page.click("[data-testid=time]");
|
||||
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', "Test Testson");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
await page.press('[name="email"]', "Enter");
|
||||
|
||||
// --- check that webhook was called
|
||||
await waitFor(() => {
|
||||
expect(webhookReceiver.requestList.length).toBe(1);
|
||||
});
|
||||
|
||||
const [request] = webhookReceiver.requestList;
|
||||
const body = request.body as any;
|
||||
|
||||
// remove dynamic properties that differs depending on where you run the tests
|
||||
const dynamic = "[redacted/dynamic]";
|
||||
body.createdAt = dynamic;
|
||||
body.payload.startTime = dynamic;
|
||||
body.payload.endTime = dynamic;
|
||||
for (const attendee of body.payload.attendees) {
|
||||
attendee.timeZone = dynamic;
|
||||
}
|
||||
body.payload.organizer.timeZone = dynamic;
|
||||
|
||||
// if we change the shape of our webhooks, we can simply update this by clicking `u`
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"createdAt": "[redacted/dynamic]",
|
||||
"payload": Object {
|
||||
"attendees": Array [
|
||||
Object {
|
||||
"email": "test@example.com",
|
||||
"name": "Test Testson",
|
||||
"timeZone": "[redacted/dynamic]",
|
||||
},
|
||||
],
|
||||
"description": "",
|
||||
"endTime": "[redacted/dynamic]",
|
||||
"organizer": Object {
|
||||
"email": "pro@example.com",
|
||||
"name": "Pro Example",
|
||||
"timeZone": "[redacted/dynamic]",
|
||||
},
|
||||
"startTime": "[redacted/dynamic]",
|
||||
"title": "30min with Test Testson",
|
||||
"type": "30min",
|
||||
},
|
||||
"triggerEvent": "BOOKING_CREATED",
|
||||
}
|
||||
`);
|
||||
|
||||
webhookReceiver.close();
|
||||
});
|
||||
});
|
|
@ -37,7 +37,7 @@ export function loginProvider(opts: {
|
|||
waitForSelector?: string;
|
||||
}): Provider<Needs, Contributes> {
|
||||
return provider<Needs, Contributes>()
|
||||
.name("page")
|
||||
.name("login")
|
||||
.before(async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export function randomString(length: number) {
|
||||
import { createServer, IncomingMessage, ServerResponse } from "http";
|
||||
|
||||
export function randomString(length = 12) {
|
||||
let result = "";
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
|
@ -7,3 +9,65 @@ export function randomString(length: number) {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type Request = IncomingMessage & { body?: unknown };
|
||||
type RequestHandlerOptions = { req: Request; res: ServerResponse };
|
||||
type RequestHandler = (opts: RequestHandlerOptions) => void;
|
||||
|
||||
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
||||
const {
|
||||
requestHandler = ({ res }) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.write(JSON.stringify({}));
|
||||
res.end();
|
||||
},
|
||||
} = opts;
|
||||
const requestList: Request[] = [];
|
||||
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);
|
||||
requestHandler({ req: _req, res });
|
||||
});
|
||||
});
|
||||
|
||||
// listen on random port
|
||||
server.listen(0);
|
||||
const port: number = (server.address() as any).port;
|
||||
const url = `http://localhost:${port}`;
|
||||
return {
|
||||
port,
|
||||
close: () => server.close(),
|
||||
requestList,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass.
|
||||
*/
|
||||
export async function waitFor(fn: () => Promise<unknown> | unknown, opts: { timeout?: number } = {}) {
|
||||
let finished = false;
|
||||
const timeout = opts.timeout ?? 5000; // 5s
|
||||
const timeStart = Date.now();
|
||||
while (!finished) {
|
||||
try {
|
||||
await fn();
|
||||
finished = true;
|
||||
} catch {
|
||||
if (Date.now() - timeStart >= timeout) {
|
||||
throw new Error("waitFor timed out");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -1779,10 +1779,15 @@
|
|||
"@types/node" "*"
|
||||
"@types/socket.io" "2.1.13"
|
||||
|
||||
"@types/node@*", "@types/node@>=8.1.0", "@types/node@^16.6.1":
|
||||
"@types/node@*", "@types/node@>=8.1.0":
|
||||
version "16.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.1.tgz#f3647623199ca920960006b3dccf633ea905f243"
|
||||
|
||||
"@types/node@^16.10.2":
|
||||
version "16.10.2"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e"
|
||||
integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==
|
||||
|
||||
"@types/nodemailer@^6.4.4":
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
|
||||
|
@ -3036,11 +3041,14 @@ data-urls@^2.0.0:
|
|||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
dayjs-business-days@^1.0.4:
|
||||
dayjs-business-time@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dayjs-business-days/-/dayjs-business-days-1.0.4.tgz#36e93e7566149e175c1541d92ce16e12145412bf"
|
||||
resolved "https://registry.npmjs.org/dayjs-business-time/-/dayjs-business-time-1.0.4.tgz#2970b80e832e92bbaa27a06ea62772b0d970b75b"
|
||||
integrity sha512-v/0ynVV0Ih9Qw/pqJdScVHfoIaHkxLSom8j9+jO+VUOPnxC0fj5QGpDAZ94LUFd7jBkq2UO8C1LrVY+EHFx3aA==
|
||||
dependencies:
|
||||
dayjs "^1.10.4"
|
||||
|
||||
dayjs@^1.10.6:
|
||||
dayjs@^1.10.4, dayjs@^1.10.6:
|
||||
version "1.10.7"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||
|
||||
|
|
Loading…
Reference in a new issue