E2E tests refactoring (#1318)
* Adds test todos * Can't seem to change locales * WIP playwright test refactoring * jest-playwright cleanup * Test fixes * Test fixes * More test fixes * WIP: Testing fixes * More test fixes * Removes unused files * Installs missing browsers for e2e * ts-node fixes * ts-check fixes * Type fixes * Fixes e2e * FFS * Renamex webhook snapshot * Fixes webhook cross-platform * Renamed webhook snapshot * Apply suggestions from code review Co-authored-by: Max Schmitt <max@schmitt.mx> * Removes kont dependency * Cleanup playwright options * Next.js cache optimizations on CI * Uses cache on e2e as well * Fixme is introducing side-effects Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: Max Schmitt <max@schmitt.mx>pull/1328/head
parent
972402be2c
commit
e6f71c81bb
|
@ -40,7 +40,11 @@ jobs:
|
|||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||
|
||||
- run: yarn prisma migrate deploy
|
||||
- run: yarn test
|
||||
|
|
|
@ -51,7 +51,11 @@ jobs:
|
|||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-nextjs
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||
|
||||
- run: yarn prisma migrate deploy
|
||||
- run: yarn db-seed
|
||||
|
@ -71,7 +75,7 @@ jobs:
|
|||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install playwright deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install-deps
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- run: yarn test-playwright
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
.nyc_output
|
||||
playwright/videos
|
||||
playwright/screenshots
|
||||
playwright/artifacts
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
|
|
|
@ -201,7 +201,7 @@ export default function Shell(props: {
|
|||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100" data-testid="dashboard-shell">
|
||||
<div className="hidden md:flex lg:flex-shrink-0">
|
||||
<div className="flex flex-col w-14 lg:w-56">
|
||||
<div className="flex flex-col flex-1 h-0 bg-white border-r border-gray-200">
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
const opts = {
|
||||
// launch headless on CI, in browser locally
|
||||
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||
collectCoverage: false, // not possible in Next.js 12
|
||||
executablePath: process.env.PLAYWRIGHT_CHROME_EXECUTABLE_PATH,
|
||||
locale: "en", // So tests won't fail if local machine is not in english
|
||||
};
|
||||
|
||||
console.log("⚙️ Playwright options:", JSON.stringify(opts, null, 4));
|
||||
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
preset: "jest-playwright-preset",
|
||||
transform: {
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
testMatch: ["<rootDir>/playwright/**/*(*.)@(spec|test).[jt]s?(x)"],
|
||||
testEnvironmentOptions: {
|
||||
"jest-playwright": {
|
||||
browsers: ["chromium" /*, 'firefox', 'webkit'*/],
|
||||
exitOnPageError: false,
|
||||
launchType: "LAUNCH",
|
||||
launchOptions: {
|
||||
headless: opts.headless,
|
||||
executablePath: opts.executablePath,
|
||||
},
|
||||
contextOptions: {
|
||||
recordVideo: {
|
||||
dir: "playwright/videos",
|
||||
},
|
||||
},
|
||||
collectCoverage: opts.collectCoverage,
|
||||
},
|
||||
},
|
||||
};
|
12
package.json
12
package.json
|
@ -10,24 +10,23 @@
|
|||
"db-up": "docker-compose up -d",
|
||||
"db-migrate": "yarn prisma migrate dev",
|
||||
"db-deploy": "yarn prisma migrate deploy",
|
||||
"db-seed": "yarn ts-node scripts/seed.ts",
|
||||
"db-seed": "ts-node scripts/seed.ts",
|
||||
"db-nuke": "docker-compose down --volumes --remove-orphans",
|
||||
"db-setup": "run-s db-up db-migrate db-seed",
|
||||
"db-reset": "run-s db-nuke db-setup",
|
||||
"deploy": "run-s build db-deploy",
|
||||
"dx": "env-cmd run-s db-setup dev",
|
||||
"test": "jest",
|
||||
"test-playwright": "jest --config jest.playwright.config.js",
|
||||
"test-playwright": "playwright test",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\"",
|
||||
"postinstall": "prisma generate",
|
||||
"pre-commit": "lint-staged",
|
||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||
"prepare": "husky install",
|
||||
"check-changed-files": "yarn ts-node scripts/ts-check-changed-files.ts"
|
||||
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.x",
|
||||
|
@ -105,6 +104,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.17.1",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "2.0.4",
|
||||
"@types/accept-language-parser": "1.5.2",
|
||||
|
@ -134,13 +134,9 @@
|
|||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^7.0.1",
|
||||
"jest": "^26.0.0",
|
||||
"jest-playwright": "^0.0.1",
|
||||
"jest-playwright-preset": "^1.7.0",
|
||||
"kont": "^0.5.1",
|
||||
"lint-staged": "^11.1.2",
|
||||
"mockdate": "^3.0.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"playwright": "^1.16.2",
|
||||
"postcss": "^8.4.4",
|
||||
"prettier": "^2.3.2",
|
||||
"prisma": "^2.30.2",
|
||||
|
|
|
@ -24,7 +24,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { useForm, Controller } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { useMutation } from "react-query";
|
||||
import Select, { OptionTypeBase } from "react-select";
|
||||
import Select from "react-select";
|
||||
|
||||
import { StripeData } from "@ee/lib/stripe/server";
|
||||
|
||||
|
@ -59,6 +59,12 @@ import * as RadioArea from "@components/ui/form/radio-area";
|
|||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: LocationType;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const addDefaultLocationOptions = (
|
||||
defaultLocations: OptionTypeBase[],
|
||||
locationOptions: OptionTypeBase[]
|
||||
|
@ -295,8 +301,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
classNamePrefix="react-select"
|
||||
className="flex-1 block w-full min-w-0 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
onChange={(e) => {
|
||||
locationFormMethods.setValue("locationType", e?.value);
|
||||
openLocationModal(e?.value);
|
||||
if (e?.value) {
|
||||
locationFormMethods.setValue("locationType", e.value);
|
||||
openLocationModal(e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -461,7 +469,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
centered
|
||||
title={t("event_type_title", { eventTypeTitle: eventType.title })}
|
||||
heading={
|
||||
<div className="relative group cursor-pointer" onClick={() => setEditIcon(false)}>
|
||||
<div className="relative cursor-pointer group" onClick={() => setEditIcon(false)}>
|
||||
{editIcon ? (
|
||||
<>
|
||||
<h1
|
||||
|
@ -469,7 +477,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
className="inline pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<PencilIcon className="-mt-1 ml-1 inline w-4 h-4 text-gray-700 group-hover:text-gray-500" />
|
||||
<PencilIcon className="inline w-4 h-4 ml-1 -mt-1 text-gray-700 group-hover:text-gray-500" />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ marginBottom: -11 }}>
|
||||
|
@ -478,7 +486,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
autoFocus
|
||||
style={{ top: -6, fontSize: 22 }}
|
||||
required
|
||||
className="w-full relative pl-0 h-10 text-gray-900 bg-transparent border-none cursor-pointer focus:text-black hover:text-gray-700 focus:ring-0 focus:outline-none"
|
||||
className="relative w-full h-10 pl-0 text-gray-900 bg-transparent border-none cursor-pointer focus:text-black hover:text-gray-700 focus:ring-0 focus:outline-none"
|
||||
placeholder={t("quick_chat")}
|
||||
{...formMethods.register("title")}
|
||||
defaultValue={eventType.title}
|
||||
|
@ -639,6 +647,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
value={asStringOrUndefined(eventType.schedulingType)}
|
||||
options={schedulingTypeOptions}
|
||||
onChange={(val) => {
|
||||
// FIXME
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
formMethods.setValue("schedulingType", val);
|
||||
}}
|
||||
/>
|
||||
|
@ -1154,8 +1165,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
classNamePrefix="react-select"
|
||||
className="flex-1 block w-full min-w-0 my-4 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
setSelectedLocation(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -417,7 +417,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-brand">
|
||||
<div className="min-h-screen bg-brand" data-testid="onboarding">
|
||||
<Head>
|
||||
<title>Cal.com - {t("getting_started")}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { PlaywrightTestConfig, devices } from "@playwright/test";
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
forbidOnly: !!process.env.CI,
|
||||
testDir: "playwright",
|
||||
timeout: 60_000,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
globalSetup: require.resolve("./playwright/lib/globalSetup"),
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
locale: "en-US",
|
||||
trace: "on-first-retry",
|
||||
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||
contextOptions: {
|
||||
recordVideo: {
|
||||
dir: "playwright/videos",
|
||||
},
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
/* {
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
}, */
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,42 +1,32 @@
|
|||
import { kont } from "kont";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
import { pageProvider } from "./lib/pageProvider";
|
||||
import { todo } from "./lib/testUtils";
|
||||
|
||||
jest.setTimeout(60e3);
|
||||
if (process.env.CI) {
|
||||
jest.retryTimes(3);
|
||||
}
|
||||
|
||||
describe("free user", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(pageProvider({ path: "/free" }))
|
||||
.done();
|
||||
|
||||
test("only one visible event", async () => {
|
||||
const { page } = ctx;
|
||||
await expect(page).toHaveSelector(`[href="/free/30min"]`);
|
||||
await expect(page).not.toHaveSelector(`[href="/free/60min"]`);
|
||||
test.describe("free user", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/free");
|
||||
});
|
||||
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.todo("`/free/30min` is bookable");
|
||||
todo("`/free/30min` is bookable");
|
||||
|
||||
test.todo("`/free/60min` is not bookable");
|
||||
todo("`/free/60min` is not bookable");
|
||||
});
|
||||
|
||||
describe("pro user", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(pageProvider({ path: "/pro" }))
|
||||
.done();
|
||||
test.describe("pro user", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/pro");
|
||||
});
|
||||
|
||||
test("pro user's page has at least 2 visible events", async () => {
|
||||
const { page } = ctx;
|
||||
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||
|
||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("book an event first day in next month", async () => {
|
||||
const { page } = ctx;
|
||||
test("book an event first day in next month", async ({ page }) => {
|
||||
// Click first event type
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
// Click [data-testid="incrementMonth"]
|
||||
|
@ -58,7 +48,7 @@ describe("pro user", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test.todo("Can reschedule the recently created booking");
|
||||
todo("Can reschedule the recently created booking");
|
||||
|
||||
test.todo("Can cancel the recently created booking");
|
||||
todo("Can cancel the recently created booking");
|
||||
});
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import { kont } from "kont";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loginProvider } from "./lib/loginProvider";
|
||||
import { randomString } from "./lib/testUtils";
|
||||
|
||||
jest.setTimeout(60e3);
|
||||
jest.retryTimes(3);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
});
|
||||
|
||||
describe("pro user", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(
|
||||
loginProvider({
|
||||
user: "pro",
|
||||
path: "/event-types",
|
||||
waitForSelector: "[data-testid=event-types]",
|
||||
})
|
||||
)
|
||||
.done();
|
||||
test.describe("pro user", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test("has at least 2 events", async () => {
|
||||
const { page } = ctx;
|
||||
test("has at least 2 events", async ({ page }) => {
|
||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||
|
||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||
|
@ -27,8 +20,7 @@ describe("pro user", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("can add new event type", async () => {
|
||||
const { page } = ctx;
|
||||
test("can add new event type", async ({ page }) => {
|
||||
await page.click("[data-testid=new-event-type]");
|
||||
const nonce = randomString(3);
|
||||
const eventTitle = `hello ${nonce}`;
|
||||
|
@ -43,25 +35,16 @@ describe("pro user", () => {
|
|||
},
|
||||
});
|
||||
|
||||
await page.goto("http://localhost:3000/event-types");
|
||||
await page.goto("/event-types");
|
||||
|
||||
await expect(page).toHaveSelector(`text='${eventTitle}'`);
|
||||
expect(page.locator(`text='${eventTitle}'`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("free user", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(
|
||||
loginProvider({
|
||||
user: "free",
|
||||
path: "/event-types",
|
||||
waitForSelector: "[data-testid=event-types]",
|
||||
})
|
||||
)
|
||||
.done();
|
||||
test.describe("free user", () => {
|
||||
test.use({ storageState: "playwright/artifacts/freeStorageState.json" });
|
||||
|
||||
test("has at least 2 events where first is enabled", async () => {
|
||||
const { page } = ctx;
|
||||
test("has at least 2 events where first is enabled", async ({ page }) => {
|
||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||
|
||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||
|
@ -71,9 +54,7 @@ describe("free user", () => {
|
|||
expect(await $last.getAttribute("data-disabled")).toBe("1");
|
||||
});
|
||||
|
||||
test("can not add new event type", async () => {
|
||||
const { page } = ctx;
|
||||
|
||||
await expect(page.$("[data-testid=new-event-type]")).toBeDisabled();
|
||||
test("can not add new event type", async ({ page }) => {
|
||||
await expect(page.locator("[data-testid=new-event-type]")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,41 +1,44 @@
|
|||
import { kont } from "kont";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loginProvider } from "./lib/loginProvider";
|
||||
import { createHttpServer, waitFor } from "./lib/testUtils";
|
||||
import { createHttpServer, todo, waitFor } from "./lib/testUtils";
|
||||
|
||||
jest.setTimeout(60e3);
|
||||
jest.retryTimes(3);
|
||||
test.describe("integrations", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
describe("webhooks", () => {
|
||||
const ctx = kont()
|
||||
.useBeforeEach(
|
||||
loginProvider({
|
||||
user: "pro",
|
||||
path: "/integrations",
|
||||
waitForSelector: '[data-testid="new_webhook"]',
|
||||
})
|
||||
)
|
||||
.done();
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/integrations");
|
||||
});
|
||||
|
||||
test("add webhook & test that creating an event triggers a webhook call", async () => {
|
||||
const { page } = ctx;
|
||||
todo("Can add Zoom integration");
|
||||
|
||||
todo("Can add Stripe integration");
|
||||
|
||||
todo("Can add Google Calendar");
|
||||
|
||||
todo("Can add Office 365 Calendar");
|
||||
|
||||
todo("Can add CalDav Calendar");
|
||||
|
||||
todo("Can add Apple Calendar");
|
||||
|
||||
test("add webhook & test that creating an event triggers a webhook call", async ({ page }, testInfo) => {
|
||||
const webhookReceiver = createHttpServer();
|
||||
|
||||
// --- add webhook
|
||||
await page.click('[data-testid="new_webhook"]');
|
||||
await expect(page).toHaveSelector("[data-testid='WebhookDialogForm']");
|
||||
expect(page.locator(`[data-testid='WebhookDialogForm']`)).toBeVisible();
|
||||
|
||||
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||
|
||||
await page.click("[type=submit]");
|
||||
|
||||
// dialog is closed
|
||||
await expect(page).not.toHaveSelector("[data-testid='WebhookDialogForm']");
|
||||
expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible();
|
||||
// page contains the url
|
||||
await expect(page).toHaveSelector(`text='${webhookReceiver.url}'`);
|
||||
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||
|
||||
// --- Book the first available day next month in the pro user's "30min"-event
|
||||
await page.goto(`http://localhost:3000/pro/30min`);
|
||||
await page.goto(`/pro/30min`);
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||
await page.click('[data-testid="time"]');
|
||||
|
@ -67,35 +70,9 @@ describe("webhooks", () => {
|
|||
|
||||
// if we change the shape of our webhooks, we can simply update this by clicking `u`
|
||||
// console.log("BODY", body);
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"createdAt": "[redacted/dynamic]",
|
||||
"payload": Object {
|
||||
"additionInformation": "[redacted/dynamic]",
|
||||
"attendees": Array [
|
||||
Object {
|
||||
"email": "test@example.com",
|
||||
"name": "Test Testson",
|
||||
"timeZone": "[redacted/dynamic]",
|
||||
},
|
||||
],
|
||||
"description": "",
|
||||
"destinationCalendar": null,
|
||||
"endTime": "[redacted/dynamic]",
|
||||
"metadata": Object {},
|
||||
"organizer": Object {
|
||||
"email": "pro@example.com",
|
||||
"name": "Pro Example",
|
||||
"timeZone": "[redacted/dynamic]",
|
||||
},
|
||||
"startTime": "[redacted/dynamic]",
|
||||
"title": "30min between Pro Example and Test Testson",
|
||||
"type": "30min",
|
||||
"uid": "[redacted/dynamic]",
|
||||
},
|
||||
"triggerEvent": "BOOKING_CREATED",
|
||||
}
|
||||
`);
|
||||
// Text files shouldn't have platform specific suffixes
|
||||
testInfo.snapshotSuffix = "";
|
||||
expect(JSON.stringify(body)).toMatchSnapshot(`webhookResponse.txt`);
|
||||
|
||||
webhookReceiver.close();
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30min","title":"30min between Pro Example and Test Testson","description":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Pro Example","email":"pro@example.com","timeZone":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]"}],"destinationCalendar":null,"uid":"[redacted/dynamic]","metadata":{},"additionInformation":"[redacted/dynamic]"}}
|
|
@ -0,0 +1,37 @@
|
|||
import { Browser, chromium } from "@playwright/test";
|
||||
|
||||
async function loginAsUser(username: string, browser: Browser) {
|
||||
const page = await browser.newPage();
|
||||
await page.goto("http://localhost:3000/auth/login");
|
||||
// Click input[name="email"]
|
||||
await page.click('input[name="email"]');
|
||||
// Fill input[name="email"]
|
||||
await page.fill('input[name="email"]', `${username}@example.com`);
|
||||
// Press Tab
|
||||
await page.press('input[name="email"]', "Tab");
|
||||
// Fill input[name="password"]
|
||||
await page.fill('input[name="password"]', username);
|
||||
// Press Enter
|
||||
await page.press('input[name="password"]', "Enter");
|
||||
await page.waitForSelector(
|
||||
username === "onboarding" ? "[data-testid=onboarding]" : "[data-testid=dashboard-shell]"
|
||||
);
|
||||
// Save signed-in state to '${username}StorageState.json'.
|
||||
await page.context().storageState({ path: `playwright/artifacts/${username}StorageState.json` });
|
||||
await page.context().close();
|
||||
}
|
||||
|
||||
async function globalSetup(/* config: FullConfig */) {
|
||||
const browser = await chromium.launch();
|
||||
await loginAsUser("onboarding", browser);
|
||||
// await loginAsUser("free-first-hidden", browser);
|
||||
await loginAsUser("pro", browser);
|
||||
// await loginAsUser("trial", browser);
|
||||
await loginAsUser("free", browser);
|
||||
// await loginAsUser("usa", browser);
|
||||
// await loginAsUser("teamfree", browser);
|
||||
// await loginAsUser("teampro", browser);
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export default globalSetup;
|
|
@ -1,87 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { provider, Provider } from "kont";
|
||||
import { Page, Cookie } from "playwright";
|
||||
|
||||
/**
|
||||
* Context data that Login provder needs.
|
||||
*/
|
||||
export type Needs = {};
|
||||
|
||||
/**
|
||||
* Login provider's options.
|
||||
*/
|
||||
export type Params = {
|
||||
user: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context data that Page provider contributes.
|
||||
*/
|
||||
export type Contributes = {
|
||||
page: Page;
|
||||
};
|
||||
|
||||
const cookieCache = new Map<string, Cookie[]>();
|
||||
/**
|
||||
* Creates a new context / "incognito tab" and logs in the specified user
|
||||
*/
|
||||
export function loginProvider(opts: {
|
||||
user: string;
|
||||
/**
|
||||
* Path to navigate to after login
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* Selector to wait for to decide that the navigation is done
|
||||
*/
|
||||
waitForSelector?: string;
|
||||
}): Provider<Needs, Contributes> {
|
||||
return provider<Needs, Contributes>()
|
||||
.name("login")
|
||||
.before(async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const cachedCookies = cookieCache.get(opts.user);
|
||||
if (cachedCookies) {
|
||||
await context.addCookies(cachedCookies);
|
||||
} else {
|
||||
await page.goto("http://localhost:3000/auth/login");
|
||||
// Click input[name="email"]
|
||||
await page.click('input[name="email"]');
|
||||
// Fill input[name="email"]
|
||||
await page.fill('input[name="email"]', `${opts.user}@example.com`);
|
||||
// Press Tab
|
||||
await page.press('input[name="email"]', "Tab");
|
||||
// Fill input[name="password"]
|
||||
await page.fill('input[name="password"]', opts.user);
|
||||
// Press Enter
|
||||
await page.press('input[name="password"]', "Enter");
|
||||
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return !url.pathname.startsWith("/auth");
|
||||
},
|
||||
});
|
||||
|
||||
const cookies = await context.cookies();
|
||||
cookieCache.set(opts.user, cookies);
|
||||
}
|
||||
|
||||
if (opts.path) {
|
||||
await page.goto(`http://localhost:3000${opts.path}`);
|
||||
}
|
||||
if (opts.waitForSelector) {
|
||||
await page.waitForSelector(opts.waitForSelector);
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
context,
|
||||
};
|
||||
})
|
||||
.after(async (ctx) => {
|
||||
await ctx.page?.close();
|
||||
await ctx.context?.close();
|
||||
})
|
||||
.done();
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { provider, Provider } from "kont";
|
||||
import { Page } from "playwright";
|
||||
|
||||
/**
|
||||
* Context data that Page provder needs.
|
||||
*/
|
||||
export type Needs = {};
|
||||
|
||||
/**
|
||||
* Page provider's options.
|
||||
*/
|
||||
export type Params = {
|
||||
user: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context data that Page provider contributes.
|
||||
*/
|
||||
export type Contributes = {
|
||||
page: Page;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new context / "incognito tab" and logs in the specified user
|
||||
*/
|
||||
export function pageProvider(opts: {
|
||||
/**
|
||||
* Path to navigate to
|
||||
*/
|
||||
path: string;
|
||||
}): Provider<Needs, Contributes> {
|
||||
return provider<Needs, Contributes>()
|
||||
.name("page")
|
||||
.before(async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto(`http://localhost:3000${opts.path}`);
|
||||
|
||||
return {
|
||||
page,
|
||||
context,
|
||||
};
|
||||
})
|
||||
.after(async (ctx) => {
|
||||
await ctx.page?.close();
|
||||
await ctx.context?.close();
|
||||
})
|
||||
.done();
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
import { test } from "@playwright/test";
|
||||
import { createServer, IncomingMessage, ServerResponse } from "http";
|
||||
|
||||
export function todo(title: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
test.skip(title, () => {});
|
||||
}
|
||||
|
||||
export function randomString(length = 12) {
|
||||
let result = "";
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
jest.setTimeout(60e3);
|
||||
import { test } from "@playwright/test";
|
||||
|
||||
test("login with pro@example.com", async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto("http://localhost:3000/auth/login");
|
||||
// Click input[name="email"]
|
||||
await page.click('input[name="email"]');
|
||||
// Fill input[name="email"]
|
||||
await page.fill('input[name="email"]', `pro@example.com`);
|
||||
// Press Tab
|
||||
await page.press('input[name="email"]', "Tab");
|
||||
// Fill input[name="password"]
|
||||
await page.fill('input[name="password"]', `pro`);
|
||||
// Press Enter
|
||||
await page.press('input[name="password"]', "Enter");
|
||||
// Using logged in state from globalSteup
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test("login with pro@example.com", async ({ page }) => {
|
||||
// Try to go homepage
|
||||
await page.goto("/");
|
||||
// It should redirect you to the event-types page
|
||||
await page.waitForSelector("[data-testid=event-types]");
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
export {};
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
import { kont } from "kont";
|
||||
import { test } from "@playwright/test";
|
||||
|
||||
import { loginProvider } from "./lib/loginProvider";
|
||||
test.describe("Onboarding", () => {
|
||||
test.use({ storageState: "playwright/artifacts/onboardingStorageState.json" });
|
||||
|
||||
jest.setTimeout(60e3);
|
||||
jest.retryTimes(2);
|
||||
|
||||
const ctx = kont()
|
||||
.useBeforeEach(
|
||||
loginProvider({
|
||||
user: "onboarding",
|
||||
})
|
||||
)
|
||||
.done();
|
||||
|
||||
test("redirects to /getting-started after login", async () => {
|
||||
await ctx.page.waitForNavigation({
|
||||
test("redirects to /getting-started after login", async ({ page }) => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname === "/getting-started";
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,8 +35,6 @@
|
|||
"jsx": "preserve",
|
||||
"types": [
|
||||
"@types/jest",
|
||||
"jest-playwright-preset",
|
||||
"expect-playwright"
|
||||
],
|
||||
"allowJs": true,
|
||||
"incremental": true
|
||||
|
@ -49,5 +47,11 @@
|
|||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
],
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"types": ["node"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue