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>
This commit is contained in:
parent
972402be2c
commit
e6f71c81bb
21 changed files with 521 additions and 847 deletions
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
@ -40,7 +40,11 @@ jobs:
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/.next/cache
|
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 prisma migrate deploy
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
|
|
8
.github/workflows/e2e.yml
vendored
8
.github/workflows/e2e.yml
vendored
|
@ -51,7 +51,11 @@ jobs:
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/.next/cache
|
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 prisma migrate deploy
|
||||||
- run: yarn db-seed
|
- run: yarn db-seed
|
||||||
|
@ -71,7 +75,7 @@ jobs:
|
||||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||||
- name: Install playwright deps
|
- name: Install playwright deps
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: yarn playwright install-deps
|
run: yarn playwright install --with-deps
|
||||||
|
|
||||||
- run: yarn test-playwright
|
- run: yarn test-playwright
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,6 +14,7 @@
|
||||||
.nyc_output
|
.nyc_output
|
||||||
playwright/videos
|
playwright/videos
|
||||||
playwright/screenshots
|
playwright/screenshots
|
||||||
|
playwright/artifacts
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|
|
@ -201,7 +201,7 @@ export default function Shell(props: {
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</div>
|
</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="hidden md:flex lg:flex-shrink-0">
|
||||||
<div className="flex flex-col w-14 lg:w-56">
|
<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">
|
<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-up": "docker-compose up -d",
|
||||||
"db-migrate": "yarn prisma migrate dev",
|
"db-migrate": "yarn prisma migrate dev",
|
||||||
"db-deploy": "yarn prisma migrate deploy",
|
"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-nuke": "docker-compose down --volumes --remove-orphans",
|
||||||
"db-setup": "run-s db-up db-migrate db-seed",
|
"db-setup": "run-s db-up db-migrate db-seed",
|
||||||
"db-reset": "run-s db-nuke db-setup",
|
"db-reset": "run-s db-nuke db-setup",
|
||||||
"deploy": "run-s build db-deploy",
|
"deploy": "run-s build db-deploy",
|
||||||
"dx": "env-cmd run-s db-setup dev",
|
"dx": "env-cmd run-s db-setup dev",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test-playwright": "jest --config jest.playwright.config.js",
|
"test-playwright": "playwright test",
|
||||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\"",
|
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||||
"prepare": "husky install",
|
"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": {
|
"engines": {
|
||||||
"node": ">=14.x",
|
"node": ">=14.x",
|
||||||
|
@ -105,6 +104,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||||
|
"@playwright/test": "^1.17.1",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "2.0.4",
|
"@trivago/prettier-plugin-sort-imports": "2.0.4",
|
||||||
"@types/accept-language-parser": "1.5.2",
|
"@types/accept-language-parser": "1.5.2",
|
||||||
|
@ -134,13 +134,9 @@
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"husky": "^7.0.1",
|
"husky": "^7.0.1",
|
||||||
"jest": "^26.0.0",
|
"jest": "^26.0.0",
|
||||||
"jest-playwright": "^0.0.1",
|
|
||||||
"jest-playwright-preset": "^1.7.0",
|
|
||||||
"kont": "^0.5.1",
|
|
||||||
"lint-staged": "^11.1.2",
|
"lint-staged": "^11.1.2",
|
||||||
"mockdate": "^3.0.5",
|
"mockdate": "^3.0.5",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"playwright": "^1.16.2",
|
|
||||||
"postcss": "^8.4.4",
|
"postcss": "^8.4.4",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"prisma": "^2.30.2",
|
"prisma": "^2.30.2",
|
||||||
|
|
|
@ -24,7 +24,7 @@ import React, { useEffect, useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import Select, { OptionTypeBase } from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { StripeData } from "@ee/lib/stripe/server";
|
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(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
type OptionTypeBase = {
|
||||||
|
label: string;
|
||||||
|
value: LocationType;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const addDefaultLocationOptions = (
|
const addDefaultLocationOptions = (
|
||||||
defaultLocations: OptionTypeBase[],
|
defaultLocations: OptionTypeBase[],
|
||||||
locationOptions: OptionTypeBase[]
|
locationOptions: OptionTypeBase[]
|
||||||
|
@ -295,8 +301,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
classNamePrefix="react-select"
|
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"
|
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) => {
|
onChange={(e) => {
|
||||||
locationFormMethods.setValue("locationType", e?.value);
|
if (e?.value) {
|
||||||
openLocationModal(e?.value);
|
locationFormMethods.setValue("locationType", e.value);
|
||||||
|
openLocationModal(e.value);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -461,7 +469,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
centered
|
centered
|
||||||
title={t("event_type_title", { eventTypeTitle: eventType.title })}
|
title={t("event_type_title", { eventTypeTitle: eventType.title })}
|
||||||
heading={
|
heading={
|
||||||
<div className="relative group cursor-pointer" onClick={() => setEditIcon(false)}>
|
<div className="relative cursor-pointer group" onClick={() => setEditIcon(false)}>
|
||||||
{editIcon ? (
|
{editIcon ? (
|
||||||
<>
|
<>
|
||||||
<h1
|
<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">
|
className="inline pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</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 }}>
|
<div style={{ marginBottom: -11 }}>
|
||||||
|
@ -478,7 +486,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
autoFocus
|
autoFocus
|
||||||
style={{ top: -6, fontSize: 22 }}
|
style={{ top: -6, fontSize: 22 }}
|
||||||
required
|
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")}
|
placeholder={t("quick_chat")}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
|
@ -639,6 +647,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
value={asStringOrUndefined(eventType.schedulingType)}
|
value={asStringOrUndefined(eventType.schedulingType)}
|
||||||
options={schedulingTypeOptions}
|
options={schedulingTypeOptions}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
// FIXME
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
formMethods.setValue("schedulingType", val);
|
formMethods.setValue("schedulingType", val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1154,8 +1165,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
classNamePrefix="react-select"
|
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"
|
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) => {
|
onChange={(val) => {
|
||||||
|
if (val) {
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
locationFormMethods.setValue("locationType", val.value);
|
||||||
setSelectedLocation(val);
|
setSelectedLocation(val);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -417,7 +417,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-brand">
|
<div className="min-h-screen bg-brand" data-testid="onboarding">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Cal.com - {t("getting_started")}</title>
|
<title>Cal.com - {t("getting_started")}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
|
36
playwright.config.ts
Normal file
36
playwright.config.ts
Normal file
|
@ -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);
|
test.describe("free user", () => {
|
||||||
if (process.env.CI) {
|
test.beforeEach(async ({ page }) => {
|
||||||
jest.retryTimes(3);
|
await page.goto("/free");
|
||||||
}
|
});
|
||||||
|
test("only one visible event", async ({ page }) => {
|
||||||
describe("free user", () => {
|
await expect(page.locator(`[href="/free/30min"]`)).toBeVisible();
|
||||||
const ctx = kont()
|
await expect(page.locator(`[href="/free/60min"]`)).not.toBeVisible();
|
||||||
.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.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", () => {
|
test.describe("pro user", () => {
|
||||||
const ctx = kont()
|
test.beforeEach(async ({ page }) => {
|
||||||
.useBeforeEach(pageProvider({ path: "/pro" }))
|
await page.goto("/pro");
|
||||||
.done();
|
});
|
||||||
|
|
||||||
test("pro user's page has at least 2 visible events", async () => {
|
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
||||||
const { page } = ctx;
|
|
||||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||||
|
|
||||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("book an event first day in next month", async () => {
|
test("book an event first day in next month", async ({ page }) => {
|
||||||
const { page } = ctx;
|
|
||||||
// Click first event type
|
// Click first event type
|
||||||
await page.click('[data-testid="event-type-link"]');
|
await page.click('[data-testid="event-type-link"]');
|
||||||
// Click [data-testid="incrementMonth"]
|
// 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";
|
import { randomString } from "./lib/testUtils";
|
||||||
|
|
||||||
jest.setTimeout(60e3);
|
test.beforeEach(async ({ page }) => {
|
||||||
jest.retryTimes(3);
|
await page.goto("/event-types");
|
||||||
|
// We wait until loading is finished
|
||||||
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
|
});
|
||||||
|
|
||||||
describe("pro user", () => {
|
test.describe("pro user", () => {
|
||||||
const ctx = kont()
|
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||||
.useBeforeEach(
|
|
||||||
loginProvider({
|
|
||||||
user: "pro",
|
|
||||||
path: "/event-types",
|
|
||||||
waitForSelector: "[data-testid=event-types]",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.done();
|
|
||||||
|
|
||||||
test("has at least 2 events", async () => {
|
test("has at least 2 events", async ({ page }) => {
|
||||||
const { page } = ctx;
|
|
||||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||||
|
|
||||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||||
|
@ -27,8 +20,7 @@ describe("pro user", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can add new event type", async () => {
|
test("can add new event type", async ({ page }) => {
|
||||||
const { page } = ctx;
|
|
||||||
await page.click("[data-testid=new-event-type]");
|
await page.click("[data-testid=new-event-type]");
|
||||||
const nonce = randomString(3);
|
const nonce = randomString(3);
|
||||||
const eventTitle = `hello ${nonce}`;
|
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", () => {
|
test.describe("free user", () => {
|
||||||
const ctx = kont()
|
test.use({ storageState: "playwright/artifacts/freeStorageState.json" });
|
||||||
.useBeforeEach(
|
|
||||||
loginProvider({
|
|
||||||
user: "free",
|
|
||||||
path: "/event-types",
|
|
||||||
waitForSelector: "[data-testid=event-types]",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.done();
|
|
||||||
|
|
||||||
test("has at least 2 events where first is enabled", async () => {
|
test("has at least 2 events where first is enabled", async ({ page }) => {
|
||||||
const { page } = ctx;
|
|
||||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||||
|
|
||||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||||
|
@ -71,9 +54,7 @@ describe("free user", () => {
|
||||||
expect(await $last.getAttribute("data-disabled")).toBe("1");
|
expect(await $last.getAttribute("data-disabled")).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can not add new event type", async () => {
|
test("can not add new event type", async ({ page }) => {
|
||||||
const { page } = ctx;
|
await expect(page.locator("[data-testid=new-event-type]")).toBeDisabled();
|
||||||
|
|
||||||
await expect(page.$("[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, todo, waitFor } from "./lib/testUtils";
|
||||||
import { createHttpServer, waitFor } from "./lib/testUtils";
|
|
||||||
|
|
||||||
jest.setTimeout(60e3);
|
test.describe("integrations", () => {
|
||||||
jest.retryTimes(3);
|
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||||
|
|
||||||
describe("webhooks", () => {
|
test.beforeEach(async ({ page }) => {
|
||||||
const ctx = kont()
|
await page.goto("/integrations");
|
||||||
.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 () => {
|
todo("Can add Zoom integration");
|
||||||
const { page } = ctx;
|
|
||||||
|
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();
|
const webhookReceiver = createHttpServer();
|
||||||
|
|
||||||
// --- add webhook
|
// --- add webhook
|
||||||
await page.click('[data-testid="new_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.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||||
|
|
||||||
await page.click("[type=submit]");
|
await page.click("[type=submit]");
|
||||||
|
|
||||||
// dialog is closed
|
// dialog is closed
|
||||||
await expect(page).not.toHaveSelector("[data-testid='WebhookDialogForm']");
|
expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible();
|
||||||
// page contains the url
|
// 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
|
// --- 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="incrementMonth"]');
|
||||||
await page.click('[data-testid="day"][data-disabled="false"]');
|
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||||
await page.click('[data-testid="time"]');
|
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`
|
// if we change the shape of our webhooks, we can simply update this by clicking `u`
|
||||||
// console.log("BODY", body);
|
// console.log("BODY", body);
|
||||||
expect(body).toMatchInlineSnapshot(`
|
// Text files shouldn't have platform specific suffixes
|
||||||
Object {
|
testInfo.snapshotSuffix = "";
|
||||||
"createdAt": "[redacted/dynamic]",
|
expect(JSON.stringify(body)).toMatchSnapshot(`webhookResponse.txt`);
|
||||||
"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",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
webhookReceiver.close();
|
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]"}}
|
37
playwright/lib/globalSetup.ts
Normal file
37
playwright/lib/globalSetup.ts
Normal file
|
@ -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";
|
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) {
|
export function randomString(length = 12) {
|
||||||
let result = "";
|
let result = "";
|
||||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
|
@ -1,23 +1,11 @@
|
||||||
jest.setTimeout(60e3);
|
import { test } from "@playwright/test";
|
||||||
|
|
||||||
test("login with pro@example.com", async () => {
|
// Using logged in state from globalSteup
|
||||||
const context = await browser.newContext();
|
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||||
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");
|
|
||||||
|
|
||||||
|
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 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);
|
test("redirects to /getting-started after login", async ({ page }) => {
|
||||||
jest.retryTimes(2);
|
await page.goto("/event-types");
|
||||||
|
await page.waitForNavigation({
|
||||||
const ctx = kont()
|
|
||||||
.useBeforeEach(
|
|
||||||
loginProvider({
|
|
||||||
user: "onboarding",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.done();
|
|
||||||
|
|
||||||
test("redirects to /getting-started after login", async () => {
|
|
||||||
await ctx.page.waitForNavigation({
|
|
||||||
url(url) {
|
url(url) {
|
||||||
return url.pathname === "/getting-started";
|
return url.pathname === "/getting-started";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,8 +35,6 @@
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"types": [
|
"types": [
|
||||||
"@types/jest",
|
"@types/jest",
|
||||||
"jest-playwright-preset",
|
|
||||||
"expect-playwright"
|
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"incremental": true
|
"incremental": true
|
||||||
|
@ -49,5 +47,11 @@
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
],
|
||||||
|
"ts-node": {
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"types": ["node"],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue