Merge branch 'main' into feat/organizations
This commit is contained in:
commit
dbb4417836
|
@ -6,13 +6,13 @@
|
|||
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getCachedResults } from "@calcom/core";
|
||||
import getCalendarsEvents from "@calcom/core/getCalendarsEvents";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
||||
export const getStaticProps: GetStaticProps<
|
||||
{ results: Awaited<ReturnType<typeof getCachedResults>> },
|
||||
{ results: Awaited<ReturnType<typeof getCalendarsEvents>> },
|
||||
{ user: string }
|
||||
> = async (context) => {
|
||||
const { user: username, month } = paramsSchema.parse(context.params);
|
||||
|
@ -33,7 +33,7 @@ export const getStaticProps: GetStaticProps<
|
|||
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
|
||||
try {
|
||||
const results = userWithCredentials?.credentials
|
||||
? await getCachedResults(
|
||||
? await getCalendarsEvents(
|
||||
userWithCredentials?.credentials,
|
||||
startDate,
|
||||
endDate,
|
||||
|
|
|
@ -48,6 +48,10 @@ class MyDocument extends Document<Props> {
|
|||
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<body
|
||||
|
|
|
@ -26,7 +26,7 @@ const decryptedSchema = z.object({
|
|||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||
const { action, token, reason } = querySchema.parse(req.query);
|
||||
const { bookingUid } = decryptedSchema.parse(
|
||||
const { bookingUid, userId } = decryptedSchema.parse(
|
||||
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||
);
|
||||
|
||||
|
@ -34,10 +34,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
|||
where: { uid: bookingUid },
|
||||
});
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
try {
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res });
|
||||
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
||||
const caller = viewerRouter.createCaller({
|
||||
...ctx,
|
||||
req,
|
||||
res,
|
||||
user: { ...user, locale: user?.locale ?? "en" },
|
||||
});
|
||||
await caller.bookings.confirm({
|
||||
bookingId: booking.id,
|
||||
recurringEventId: booking.recurringEventId || undefined,
|
||||
|
|
|
@ -9,7 +9,7 @@ test.afterEach(({ users }) => users.deleteAll());
|
|||
test.describe("App Store - Authed", () => {
|
||||
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/apps/categories/calendar");
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.waitForURL("/apps/apple-calendar");
|
||||
|
@ -19,7 +19,7 @@ test.describe("App Store - Authed", () => {
|
|||
|
||||
test("Installed Apps - Navigation", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
await page.waitForSelector('[data-testid="connect-calendar-apps"]');
|
||||
await page.click('[data-testid="vertical-tab-payment"]');
|
||||
|
|
|
@ -3,7 +3,7 @@ import { test } from "../lib/fixtures";
|
|||
test.describe("Can signup from a team invite", async () => {
|
||||
test.beforeEach(async ({ users }) => {
|
||||
const proUser = await users.create();
|
||||
await proUser.login();
|
||||
await proUser.apiLogin();
|
||||
});
|
||||
test.afterEach(async ({ users }) => users.deleteAll());
|
||||
|
||||
|
@ -33,65 +33,65 @@ test.describe("Can signup from a team invite", async () => {
|
|||
// Wait for the invite to be sent
|
||||
/*await page.waitForSelector(`[data-testid="member-email"][data-email="${testUser.email}"]`);
|
||||
|
||||
const tokenObj = await prisma.verificationToken.findFirstOrThrow({
|
||||
where: { identifier: testUser.email },
|
||||
select: { token: true },
|
||||
});
|
||||
const tokenObj = await prisma.verificationToken.findFirstOrThrow({
|
||||
where: { identifier: testUser.email },
|
||||
select: { token: true },
|
||||
});
|
||||
|
||||
if (!proUser.username) throw Error("Test username is null, can't continue");
|
||||
if (!proUser.username) throw Error("Test username is null, can't continue");
|
||||
|
||||
// Open a new user window to accept the invite
|
||||
const newPage = await browser.newPage();
|
||||
await newPage.goto(`/auth/signup?token=${tokenObj.token}&callbackUrl=${WEBAPP_URL}/settings/teams`);
|
||||
// Open a new user window to accept the invite
|
||||
const newPage = await browser.newPage();
|
||||
await newPage.goto(`/auth/signup?token=${tokenObj.token}&callbackUrl=${WEBAPP_URL}/settings/teams`);
|
||||
|
||||
// Fill in form
|
||||
await newPage.fill('input[name="username"]', proUser.username); // Invalid username
|
||||
await newPage.fill('input[name="email"]', testUser.email);
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
// Fill in form
|
||||
await newPage.fill('input[name="username"]', proUser.username); // Invalid username
|
||||
await newPage.fill('input[name="email"]', testUser.email);
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
|
||||
await expect(newPage.locator('text="Username already taken"')).toBeVisible();
|
||||
await expect(newPage.locator('text="Username already taken"')).toBeVisible();
|
||||
|
||||
// Email address is already registered
|
||||
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
|
||||
await newPage.reload();
|
||||
await newPage.fill('input[name="username"]', testUser.username);
|
||||
await newPage.fill('input[name="email"]', `${proUser.username}@example.com`); // Taken email
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
await expect(newPage.locator('text="Email address is already registered"')).toBeVisible();
|
||||
// Email address is already registered
|
||||
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
|
||||
await newPage.reload();
|
||||
await newPage.fill('input[name="username"]', testUser.username);
|
||||
await newPage.fill('input[name="email"]', `${proUser.username}@example.com`); // Taken email
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
await expect(newPage.locator('text="Email address is already registered"')).toBeVisible();
|
||||
|
||||
// Successful signup
|
||||
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
|
||||
await newPage.reload();
|
||||
await newPage.fill('input[name="username"]', testUser.username);
|
||||
await newPage.fill('input[name="email"]', testUser.email);
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
await expect(newPage.locator(`[data-testid="login-form"]`)).toBeVisible();
|
||||
// Successful signup
|
||||
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
|
||||
await newPage.reload();
|
||||
await newPage.fill('input[name="username"]', testUser.username);
|
||||
await newPage.fill('input[name="email"]', testUser.email);
|
||||
await newPage.fill('input[name="password"]', testUser.password);
|
||||
await newPage.fill('input[name="passwordcheck"]', testUser.password);
|
||||
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
|
||||
await expect(newPage.locator(`[data-testid="login-form"]`)).toBeVisible();
|
||||
|
||||
// We don't need the new browser anymore
|
||||
await newPage.close();
|
||||
// We don't need the new browser anymore
|
||||
await newPage.close();
|
||||
|
||||
const createdUser = await prisma.user.findUniqueOrThrow({
|
||||
where: { email: testUser.email },
|
||||
include: { teams: { include: { team: true } } },
|
||||
});
|
||||
const createdUser = await prisma.user.findUniqueOrThrow({
|
||||
where: { email: testUser.email },
|
||||
include: { teams: { include: { team: true } } },
|
||||
});
|
||||
|
||||
console.log("createdUser", createdUser);
|
||||
console.log("createdUser", createdUser);
|
||||
|
||||
// Check that the user was created
|
||||
expect(createdUser).not.toBeNull();
|
||||
expect(createdUser.username).toBe(testUser.username);
|
||||
expect(createdUser.password).not.toBeNull();
|
||||
expect(createdUser.emailVerified).not.toBeNull();
|
||||
// Check that the user accepted the team invite
|
||||
expect(createdUser.teams).toHaveLength(1);
|
||||
expect(createdUser.teams[0].team.name).toBe(teamName);
|
||||
expect(createdUser.teams[0].role).toBe("MEMBER");
|
||||
expect(createdUser.teams[0].accepted).toBe(true);*/
|
||||
// Check that the user was created
|
||||
expect(createdUser).not.toBeNull();
|
||||
expect(createdUser.username).toBe(testUser.username);
|
||||
expect(createdUser.password).not.toBeNull();
|
||||
expect(createdUser.emailVerified).not.toBeNull();
|
||||
// Check that the user accepted the team invite
|
||||
expect(createdUser.teams).toHaveLength(1);
|
||||
expect(createdUser.teams[0].team.name).toBe(teamName);
|
||||
expect(createdUser.teams[0].role).toBe("MEMBER");
|
||||
expect(createdUser.teams[0].accepted).toBe(true);*/
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,12 +8,10 @@ test("Can delete user account", async ({ page, users }) => {
|
|||
const user = await users.create({
|
||||
username: "delete-me",
|
||||
});
|
||||
await user.login();
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto(`/settings/my-account/profile`);
|
||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||
|
||||
await page.goto(`/settings/my-account/profile`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.click("[data-testid=delete-account]");
|
||||
|
||||
expect(user.username).toBeTruthy();
|
||||
|
|
|
@ -18,7 +18,7 @@ test("Can reset forgotten password", async ({ page, users }) => {
|
|||
|
||||
// Press Enter
|
||||
await Promise.all([
|
||||
page.waitForURL((u) => u.pathname.startsWith("/auth/forgot-password/")),
|
||||
page.waitForURL((u) => u.pathname.startsWith("/auth/forgot-password")),
|
||||
page.press('input[name="email"]', "Enter"),
|
||||
]);
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ test.describe.configure({ mode: "parallel" });
|
|||
test.describe("Availablity tests", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/availability");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="schedules"]');
|
||||
|
|
|
@ -78,7 +78,7 @@ testBothBookers.describe("pro user", () => {
|
|||
const [eventType] = pro.eventTypes;
|
||||
await bookings.create(pro.id, pro.username, eventType.id);
|
||||
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.waitForSelector('[data-testid="bookings"]');
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
|
@ -106,7 +106,7 @@ testBothBookers.describe("pro user", () => {
|
|||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
const [pro] = users.get();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/upcoming");
|
||||
await page.locator('[data-testid="cancel"]').first().click();
|
||||
|
@ -132,7 +132,7 @@ testBothBookers.describe("pro user", () => {
|
|||
}) => {
|
||||
await bookOptinEvent(page);
|
||||
const [pro] = users.get();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/unconfirmed");
|
||||
await Promise.all([
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
selectFirstAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
|
||||
|
@ -50,17 +51,27 @@ async function createUserWithSeatedEventAndAttendees(
|
|||
testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
||||
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
|
||||
const user = await users.create({ name: "Seated event" });
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
const eventTitle = "My 2-seated event";
|
||||
await createNewSeatedEventType(page, { eventTitle });
|
||||
await expect(page.locator(`text=${eventTitle} event type updated successfully`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
|
||||
const slug = "my-2-seated-event";
|
||||
const user = await users.create({
|
||||
name: "Seated event user",
|
||||
eventTypes: [
|
||||
{ title: "My 2-seated event", slug, length: 60, seatsPerTimeSlot: 2, seatsShowAttendees: true },
|
||||
{
|
||||
title: "My 2-seated event",
|
||||
slug,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 2,
|
||||
seatsShowAttendees: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.goto(`/${user.username}/${slug}`);
|
||||
|
@ -93,6 +104,7 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
|||
await expect(page.locator("[data-testid=success-page]")).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Make E2E test: Attendee #1 should be able to cancel his booking
|
||||
// todo("Attendee #1 should be able to cancel his booking");
|
||||
// TODO: Make E2E test: Attendee #1 should be able to reschedule his booking
|
||||
|
@ -222,7 +234,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
|||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
]);
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
const oldBooking = await prisma.booking.findFirst({
|
||||
where: { uid: booking.uid },
|
||||
|
@ -328,7 +340,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
|||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
]);
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
const bookingAttendees = await prisma.attendee.findMany({
|
||||
where: { bookingId: booking.id },
|
||||
|
@ -379,7 +391,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
|
|||
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||
]);
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
const bookingWithEventType = await prisma.booking.findFirst({
|
||||
where: { uid: booking.uid },
|
||||
select: {
|
||||
|
|
|
@ -7,7 +7,7 @@ test.afterEach(({ users }) => users.deleteAll());
|
|||
test.describe("Change Password Test", () => {
|
||||
test("change password", async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
// Go to http://localhost:3000/settings/security
|
||||
await page.goto("/settings/security/password");
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ test.describe("Change username on settings", () => {
|
|||
test.fixme("User can change username", async ({ page, users, prisma }) => {
|
||||
const user = await users.create();
|
||||
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
// Try to go homepage
|
||||
await page.goto("/settings/my-account/profile");
|
||||
// Change username from normal to normal
|
||||
|
@ -56,7 +56,7 @@ test.describe("Change username on settings", () => {
|
|||
const user = await users.create();
|
||||
await stripe.customers.create({ email: `${user?.username}@example.com` });
|
||||
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
// Change username from normal to premium
|
||||
|
|
|
@ -14,7 +14,7 @@ test.afterEach(({ users }) => users.deleteAll());
|
|||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip("dynamic booking", async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
|
||||
const free = await users.create({ username: "free" });
|
||||
await page.goto(`/${pro.username}+${free.username}`);
|
||||
|
|
|
@ -47,7 +47,15 @@ async function expectToBeNavigatingToEmbedTypesDialog(
|
|||
|
||||
async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
|
||||
page: Page,
|
||||
{ embedUrl, embedType, basePage }: { embedUrl: string | null; embedType: string; basePage: string }
|
||||
{
|
||||
embedUrl,
|
||||
embedType,
|
||||
basePage,
|
||||
}: {
|
||||
embedUrl: string | null;
|
||||
embedType: string;
|
||||
basePage: string;
|
||||
}
|
||||
) {
|
||||
if (!embedUrl) {
|
||||
throw new Error("Couldn't find embedUrl");
|
||||
|
@ -89,7 +97,7 @@ test.afterEach(({ users }) => users.deleteAll());
|
|||
test.describe("Embed Code Generator Tests", () => {
|
||||
test.beforeEach(async ({ users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
});
|
||||
|
||||
test.describe("Event Types Page", () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ test.describe("Event Types tests", () => {
|
|||
testBothBookers.describe("user", (bookerVariant) => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
|
|
|
@ -262,13 +262,17 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
// self is a reflective method that return the Prisma object that references this fixture.
|
||||
const self = async () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
(await prisma.user.findUnique({ where: { id: store.user.id }, include: { eventTypes: true } }))!;
|
||||
(await prisma.user.findUnique({
|
||||
where: { id: store.user.id },
|
||||
include: { eventTypes: true },
|
||||
}))!;
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
eventTypes: user.eventTypes,
|
||||
routingForms: user.routingForms,
|
||||
self,
|
||||
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
|
@ -276,7 +280,10 @@ const createUserFixture = (user: UserWithIncludes, page: 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 } } });
|
||||
await prisma.user.update({
|
||||
where: { id: store.user.id },
|
||||
data: { metadata: { debug: message } },
|
||||
});
|
||||
},
|
||||
delete: async () => await prisma.user.delete({ where: { id: store.user.id } }),
|
||||
};
|
||||
|
@ -342,6 +349,28 @@ export async function login(
|
|||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
export async function apiLogin(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
page: Page
|
||||
) {
|
||||
const csrfToken = await page
|
||||
.context()
|
||||
.request.get("/api/auth/csrf")
|
||||
.then((response) => response.json())
|
||||
.then((json) => json.csrfToken);
|
||||
const data = {
|
||||
email: user.email ?? `${user.username}@example.com`,
|
||||
password: user.password ?? user.username!,
|
||||
callbackURL: "http://localhost:3000/",
|
||||
redirect: "false",
|
||||
json: "true",
|
||||
csrfToken,
|
||||
};
|
||||
return page.context().request.post("/api/auth/callback/credentials", {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPaymentCredential(page: Page) {
|
||||
await page.goto("/apps/stripe");
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ test.describe.configure({ mode: "parallel" });
|
|||
test.fixme("hash my url", () => {
|
||||
test.beforeEach(async ({ users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
});
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
|
|
|
@ -24,7 +24,7 @@ test.describe("Stripe integration", () => {
|
|||
test.describe("Stripe integration dashboard", () => {
|
||||
test("Can add Stripe integration", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
await user.getPaymentCredential();
|
||||
|
@ -39,7 +39,7 @@ test.describe("Stripe integration", () => {
|
|||
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 user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
await user.getPaymentCredential();
|
||||
|
||||
|
|
|
@ -214,7 +214,7 @@ test.fixme("Integrations", () => {
|
|||
test.describe("Zoom App", () => {
|
||||
test("Can add integration", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await addZoomIntegration({ page });
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
|
@ -226,7 +226,7 @@ test.fixme("Integrations", () => {
|
|||
|
||||
test("can choose zoom as a location during booking", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
const eventType = await addLocationIntegrationToFirstEvent({ user });
|
||||
await addZoomIntegration({ page });
|
||||
await page.waitForNavigation({
|
||||
|
@ -241,7 +241,7 @@ test.fixme("Integrations", () => {
|
|||
// POST https://api.zoom.us/v2/users/me/meetings
|
||||
// Verify Header-> Authorization: "Bearer " + accessToken,
|
||||
/**
|
||||
* {
|
||||
* {
|
||||
topic: event.title,
|
||||
type: 2, // Means that this is a scheduled meeting
|
||||
start_time: event.startTime,
|
||||
|
@ -262,15 +262,15 @@ test.fixme("Integrations", () => {
|
|||
approval_type: 2,
|
||||
audio: "both",
|
||||
auto_recording: "none",
|
||||
enforce_login: false,
|
||||
enforce_apiLogin: false,
|
||||
registrants_email_notification: true,
|
||||
},
|
||||
};
|
||||
*/
|
||||
*/
|
||||
});
|
||||
test("Can disconnect from integration", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await addZoomIntegration({ page });
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
|
@ -299,7 +299,7 @@ test.fixme("Integrations", () => {
|
|||
test.describe("Hubspot App", () => {
|
||||
test("Can add integration", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await addOauthBasedIntegration({
|
||||
page,
|
||||
slug: "hubspot",
|
||||
|
|
|
@ -40,7 +40,7 @@ declare global {
|
|||
* @see https://playwright.dev/docs/test-fixtures
|
||||
*/
|
||||
export const test = base.extend<Fixtures>({
|
||||
users: async ({ page }, use, workerInfo) => {
|
||||
users: async ({ page, context }, use, workerInfo) => {
|
||||
const usersFixture = createUsersFixture(page, workerInfo);
|
||||
await use(usersFixture);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Login with api request", () => {
|
||||
test("context request will share cookie storage with its browser context", async ({ page, users }) => {
|
||||
const pro = await users.create();
|
||||
await pro.apiLogin();
|
||||
|
||||
const contextCookies = await page.context().cookies();
|
||||
const cookiesMap = new Map(contextCookies.map(({ name, value }) => [name, value]));
|
||||
|
||||
// The browser context will already contain all the cookies from the API response.
|
||||
expect(cookiesMap.has("next-auth.csrf-token")).toBeTruthy();
|
||||
expect(cookiesMap.has("next-auth.callback-url")).toBeTruthy();
|
||||
expect(cookiesMap.has("next-auth.session-token")).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -15,6 +15,7 @@ async function getLabelText(field: Locator) {
|
|||
return await field.locator("label").first().locator("span").first().innerText();
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
test.describe("Manage Booking Questions", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
|
@ -28,7 +29,7 @@ test.describe("Manage Booking Questions", () => {
|
|||
}, testInfo) => {
|
||||
// Considering there are many steps in it, it would need more than default test timeout
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
const user = await createAndLoginUserWithEventTypes({ users });
|
||||
const user = await createAndLoginUserWithEventTypes({ users, page });
|
||||
|
||||
const webhookReceiver = await addWebhook(user);
|
||||
|
||||
|
@ -51,7 +52,7 @@ test.describe("Manage Booking Questions", () => {
|
|||
}, testInfo) => {
|
||||
// Considering there are many steps in it, it would need more than default test timeout
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
const user = await createAndLoginUserWithEventTypes({ users });
|
||||
const user = await createAndLoginUserWithEventTypes({ users, page });
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
members: {
|
||||
|
@ -247,6 +248,7 @@ async function bookTimeSlot({ page, name, email }: { page: Page; name: string; e
|
|||
await page.fill('[name="email"]', email);
|
||||
await page.press('[name="email"]', "Enter");
|
||||
}
|
||||
|
||||
/**
|
||||
* 'option' starts from 1
|
||||
*/
|
||||
|
@ -358,11 +360,20 @@ async function toggleQuestionRequireStatusAndSave({
|
|||
await saveEventType(page);
|
||||
}
|
||||
|
||||
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
||||
async function createAndLoginUserWithEventTypes({
|
||||
users,
|
||||
page,
|
||||
}: {
|
||||
users: ReturnType<typeof createUsersFixture>;
|
||||
page: Page;
|
||||
}) {
|
||||
const user = await users.create(null, {
|
||||
hasTeam: true,
|
||||
});
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ test.describe("Managed Event Types tests", () => {
|
|||
// Creating the member user of the team
|
||||
const memberUser = await users.create();
|
||||
// First we work with owner user, logging in
|
||||
await adminUser.login();
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// Making sure page loads completely
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Let's create a team
|
||||
|
@ -58,7 +59,9 @@ test.describe("Managed Event Types tests", () => {
|
|||
// Now we need to accept the invitation as member and come back in as admin to
|
||||
// assign the member in the managed event type
|
||||
await adminUser.logout();
|
||||
await memberUser.login();
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await page.goto("/teams");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
@ -67,7 +70,8 @@ test.describe("Managed Event Types tests", () => {
|
|||
await memberUser.logout();
|
||||
|
||||
// Coming back as team owner to assign member user to managed event
|
||||
await adminUser.login();
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator('[data-testid="event-types"] a[title="managed"]').click();
|
||||
await page.locator('[data-testid="vertical-tab-assignment"]').click();
|
||||
|
@ -78,7 +82,8 @@ test.describe("Managed Event Types tests", () => {
|
|||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.login();
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('[data-testid="event-types"] a[title="managed"]')).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -11,8 +11,8 @@ test.describe("Onboarding", () => {
|
|||
test.describe("Onboarding v2", () => {
|
||||
test("Onboarding Flow", async ({ page, users }) => {
|
||||
const user = await users.create({ completedOnboarding: false, name: null });
|
||||
await user.login();
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto("/getting-started");
|
||||
// tests whether the user makes it to /getting-started
|
||||
// after login with completedOnboarding false
|
||||
await page.waitForURL("/getting-started");
|
||||
|
|
|
@ -25,7 +25,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
|||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/bookings/upcoming");
|
||||
|
||||
await page.locator('[data-testid="edit_booking"]').nth(0).click();
|
||||
|
@ -69,7 +69,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
|||
rescheduled: true,
|
||||
});
|
||||
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/bookings/cancelled");
|
||||
|
||||
const requestRescheduleSentElement = page.locator('[data-testid="request_reschedule_sent"]').nth(1);
|
||||
|
@ -117,7 +117,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
|||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(!IS_STRIPE_ENABLED, "Skipped as Stripe is not installed");
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await user.getPaymentCredential();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid")!;
|
||||
|
@ -158,7 +158,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
|||
|
||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await user.getPaymentCredential();
|
||||
await users.logout();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
@ -207,7 +207,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
|
|||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ test.describe("Teams", () => {
|
|||
test("Can create teams via Wizard", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto("/teams");
|
||||
|
||||
await test.step("Can create team", async () => {
|
||||
|
|
|
@ -18,7 +18,7 @@ test.describe("BOOKING_CREATED", async () => {
|
|||
const webhookReceiver = createHttpServer();
|
||||
const user = await users.create();
|
||||
const [eventType] = user.eventTypes;
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto(`/settings/developer/webhooks`);
|
||||
|
||||
// --- add webhook
|
||||
|
@ -154,7 +154,7 @@ test.describe("BOOKING_REJECTED", async () => {
|
|||
await bookOptinEvent(page);
|
||||
|
||||
// --- login as that user
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto(`/settings/developer/webhooks`);
|
||||
|
||||
|
@ -275,7 +275,7 @@ test.describe("BOOKING_REQUESTED", async () => {
|
|||
const user = await users.create();
|
||||
|
||||
// --- login as that user
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto(`/settings/developer/webhooks`);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ test.describe("Wipe my Cal App Test", () => {
|
|||
);
|
||||
await bookings.create(pro.id, pro.username, eventType.id, {});
|
||||
await bookings.create(pro.id, pro.username, eventType.id, {});
|
||||
await pro.login();
|
||||
await pro.apiLogin();
|
||||
await page.goto("/bookings/upcoming");
|
||||
await expect(page.locator("data-testid=wipe-today-button")).toBeVisible();
|
||||
|
||||
|
|
|
@ -294,6 +294,18 @@
|
|||
"success": "Succès",
|
||||
"failed": "Échoué",
|
||||
"password_has_been_reset_login": "Votre mot de passe a été réinitialisé. Vous pouvez désormais vous connecter avec votre nouveau mot de passe.",
|
||||
"bookerlayout_title": "Mise en page",
|
||||
"bookerlayout_default_title": "Vue par défaut",
|
||||
"bookerlayout_description": "Vous pouvez en sélectionner plusieurs, vos utilisateurs peuvent basculer entre les vues.",
|
||||
"bookerlayout_user_settings_title": "Mise en page de réservation",
|
||||
"bookerlayout_user_settings_description": "Vous pouvez en sélectionner plusieurs, les utilisateurs peuvent basculer entre les vues. Cela peut être remplacé individuellement pour événement.",
|
||||
"bookerlayout_month_view": "Mensuelle",
|
||||
"bookerlayout_week_view": "Hebdomadaire",
|
||||
"bookerlayout_column_view": "Colonne",
|
||||
"bookerlayout_error_min_one_enabled": "Au moins une mise en page doit être activée.",
|
||||
"bookerlayout_error_default_not_enabled": "La mise en page sélectionnée comme vue par défaut ne fait pas partie des mises en page activées.",
|
||||
"bookerlayout_error_unknown_layout": "La mise en page que vous avez sélectionnée n'est pas une mise en page valide.",
|
||||
"bookerlayout_override_global_settings": "Vous pouvez gérer cela pour tous vos types d'événements dans <2>Paramètres / Apparence</2> ou <6>l'appliquer uniquement à cet événement</6>.",
|
||||
"unexpected_error_try_again": "Une erreur inattendue s'est produite. Veuillez réessayer.",
|
||||
"sunday_time_error": "Heure non valide le dimanche",
|
||||
"monday_time_error": "Créneau invalide le lundi",
|
||||
|
@ -760,6 +772,9 @@
|
|||
"new_event_type_to_book_description": "Créez un nouveau type d’événement pour que les personnes puissent effectuer des réservations.",
|
||||
"length": "Durée",
|
||||
"minimum_booking_notice": "Préavis minimum",
|
||||
"offset_toggle": "Décalage de l'heure de début",
|
||||
"offset_toggle_description": "Appliquez un décalage de durée spécifique aux créneaux horaires affichés aux utilisateurs.",
|
||||
"offset_start": "Décalage de",
|
||||
"offset_start_description": "p. ex. cela affichera les créneaux à vos utilisateurs à {{ adjustedTime }} au lieu de {{ originalTime }}",
|
||||
"slot_interval": "Fréquence de créneaux horaires",
|
||||
"slot_interval_default": "Utiliser la durée de l'événement (par défaut)",
|
||||
|
@ -1268,7 +1283,7 @@
|
|||
"connect_automation_apps": "Connecter des applications d'automatisation",
|
||||
"connect_analytics_apps": "Connecter des applications analytiques",
|
||||
"connect_other_apps": "Connecter d'autres applications",
|
||||
"connect_web3_apps": "Connecter des apps Web3",
|
||||
"connect_web3_apps": "Connecter des applications Web3",
|
||||
"current_step_of_total": "Étape {{currentStep}} sur {{maxSteps}}",
|
||||
"add_variable": "Ajouter une variable",
|
||||
"custom_phone_number": "Numéro de téléphone personnalisé",
|
||||
|
@ -1288,6 +1303,7 @@
|
|||
"download_responses_description": "Téléchargez toutes les réponses à votre formulaire au format CSV.",
|
||||
"download": "Télécharger",
|
||||
"download_recording": "Télécharger l'enregistrement",
|
||||
"recording_from_your_recent_call": "Un enregistrement de votre appel récent sur {{appName}} est prêt à être téléchargé",
|
||||
"create_your_first_form": "Créez votre premier formulaire",
|
||||
"create_your_first_form_description": "Avec les formulaires de routage, vous pouvez poser des questions de qualification et rediriger vers la bonne personne ou le bon type d'événement.",
|
||||
"create_your_first_webhook": "Créez votre premier webhook",
|
||||
|
@ -1483,6 +1499,8 @@
|
|||
"find_the_best_person": "Trouvez la meilleure personne disponible et alternez à travers votre équipe.",
|
||||
"fixed_round_robin": "Round-Robin fixe",
|
||||
"add_one_fixed_attendee": "Ajoutez un participant fixe et le Round-Robin via un certain nombre de participants.",
|
||||
"calcom_is_better_with_team": "{{appName}} est meilleur en équipe",
|
||||
"the_calcom_team": "L'équipe {{companyName}}",
|
||||
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
|
||||
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
|
||||
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
|
||||
|
@ -1501,6 +1519,7 @@
|
|||
"navigate_installed_apps": "Accéder aux applications installées",
|
||||
"disabled_calendar": "Si un autre calendrier est installé, les nouvelles réservations y seront ajoutées. Si ce n'est pas le cas, connectez un nouveau calendrier pour ne pas manquer de nouvelles réservations.",
|
||||
"enable_apps": "Activer les applications",
|
||||
"enable_apps_description": "Activer les applications que les utilisateurs peuvent intégrer à {{appName}}",
|
||||
"purchase_license": "Acheter une licence",
|
||||
"already_have_key": "J'ai déjà une clé :",
|
||||
"already_have_key_suggestion": "Veuillez copier votre variable d'environnement CALCOM_LICENSE_KEY existante ici.",
|
||||
|
|
|
@ -18,6 +18,8 @@ import type {
|
|||
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
import getCalendarsEvents from "./getCalendarsEvents";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
let coldStart = true;
|
||||
|
||||
|
@ -236,7 +238,7 @@ export const getBusyCalendarTimes = async (
|
|||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
|
||||
results = await getCachedResults(withCredentials, startDate, endDate, selectedCalendars);
|
||||
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars);
|
||||
logger.info("Generating calendar cache in background");
|
||||
// on cold start the calendar cache page generated in the background
|
||||
Promise.all(months.map((month) => createCalendarCachePage(username, month)));
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import type { SelectedCalendar } from "@prisma/client";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
const getCalendarsEvents = async (
|
||||
withCredentials: CredentialPayload[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
): Promise<EventBusyDate[][]> => {
|
||||
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
|
||||
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential)));
|
||||
performance.mark("getBusyCalendarTimesStart");
|
||||
const results = calendars.map(async (c, i) => {
|
||||
/** Filter out nulls */
|
||||
if (!c) return [];
|
||||
/** We rely on the index so we can match credentials with calendars */
|
||||
const { type, appId } = calendarCredentials[i];
|
||||
/** We just pass the calendars that matched the credential type,
|
||||
* TODO: Migrate credential type or appId
|
||||
*/
|
||||
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
|
||||
if (!passedSelectedCalendars.length) return [];
|
||||
/** We extract external Ids so we don't cache too much */
|
||||
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
|
||||
/** If we don't then we actually fetch external calendars (which can be very slow) */
|
||||
performance.mark("eventBusyDatesStart");
|
||||
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
performance.mark("eventBusyDatesEnd");
|
||||
performance.measure(
|
||||
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
|
||||
"eventBusyDatesStart",
|
||||
"eventBusyDatesEnd"
|
||||
);
|
||||
|
||||
return eventBusyDates.map((a) => ({ ...a, source: `${appId}` }));
|
||||
});
|
||||
const awaitedResults = await Promise.all(results);
|
||||
performance.mark("getBusyCalendarTimesEnd");
|
||||
performance.measure(
|
||||
`getBusyCalendarTimes took $1 for creds ${calendarCredentials.map((cred) => cred.id)}`,
|
||||
"getBusyCalendarTimesStart",
|
||||
"getBusyCalendarTimesEnd"
|
||||
);
|
||||
return awaitedResults;
|
||||
};
|
||||
|
||||
export default getCalendarsEvents;
|
15
yarn.lock
15
yarn.lock
|
@ -5044,7 +5044,7 @@ __metadata:
|
|||
next: ~13.2.1
|
||||
next-auth: ^4.20.1
|
||||
next-i18next: ^11.3.0
|
||||
playwright: ^1.31.2
|
||||
playwright-core: ^1.34.3
|
||||
postcss: ^8.4.18
|
||||
prism-react-renderer: ^1.3.5
|
||||
react: ^18.2.0
|
||||
|
@ -29630,7 +29630,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.34.3":
|
||||
"playwright-core@npm:^1.34.3":
|
||||
version: 1.34.3
|
||||
resolution: "playwright-core@npm:1.34.3"
|
||||
bin:
|
||||
|
@ -29639,17 +29639,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright@npm:^1.31.2":
|
||||
version: 1.34.3
|
||||
resolution: "playwright@npm:1.34.3"
|
||||
dependencies:
|
||||
playwright-core: 1.34.3
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 4495b23eacc673c03fd4706ce5914dd4855d46657e63411e54bb928e796d7ca59a6101379000ec73e2731437d04a441242cebbb6d4e069e050255db9eff65f7d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
|
||||
version: 3.4.0
|
||||
resolution: "pngjs@npm:3.4.0"
|
||||
|
|
Loading…
Reference in New Issue
Block a user