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:
authored and GitHub committed 2021-12-15 16:25:49 +00:00
1 parent 972402be2c
commit e6f71c81bb
21 files changed
+521 -847

No files matched your search

+19 -29
View File
@@ -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");
});
+17 -36
View File
@@ -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();
});
});
+27 -50
View File
@@ -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]"}}
+37
View 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;
-87
View File
@@ -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();
}
-51
View File
@@ -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();
}
+6
View File
@@ -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";
+7 -19
View File
@@ -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 {};
+10 -18
View File
@@ -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";
},
});
});
});