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:
21 files changed
+521
-847
No files matched your search
@@ -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({
|
||||
url(url) {
|
||||
return url.pathname === "/getting-started";
|
||||
},
|
||||
test("redirects to /getting-started after login", async ({ page }) => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname === "/getting-started";
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user