test: improve flaky E2E tests (#26473)
* fix flakes * revert * fix * update * more update * fix * revert --------- Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
co-authored by
Anik Dhabal Babu
Anik Dhabal Babu
parent
a68fcf8840
commit
27623926fc
@@ -117,6 +117,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
type="submit"
|
||||
className="mt-8 flex w-full flex-row justify-center"
|
||||
loading={mutation.isPending}
|
||||
data-testid="connect-calendar-button"
|
||||
disabled={mutation.isPending}>
|
||||
{t("connect_your_calendar")}
|
||||
</Button>
|
||||
|
||||
@@ -51,7 +51,7 @@ test.describe("Admin Users Management", () => {
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
await expect(page.locator('input[name="name"]')).toHaveValue("Edit User");
|
||||
await expect(page.locator('input[name="name"]').first()).toHaveValue("Edit User");
|
||||
|
||||
await page.fill('input[name="name"]', "Updated User");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ test("Can delete user account", async ({ page, users }) => {
|
||||
});
|
||||
await user.apiLogin();
|
||||
await page.goto(`/settings/my-account/profile`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||
|
||||
await page.click("[data-testid=delete-account]");
|
||||
|
||||
@@ -6,6 +6,38 @@ import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
/**
|
||||
* Helper to retry network requests that may fail with transient errors like ECONNRESET
|
||||
*/
|
||||
async function retryOnNetworkError<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries = 3,
|
||||
delayMs = 500
|
||||
): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
const errorMessage = lastError.message || "";
|
||||
// Only retry on transient network errors
|
||||
const isRetryable =
|
||||
errorMessage.includes("ECONNRESET") ||
|
||||
errorMessage.includes("ECONNREFUSED") ||
|
||||
errorMessage.includes("ETIMEDOUT") ||
|
||||
errorMessage.includes("socket hang up");
|
||||
|
||||
if (!isRetryable || attempt === maxRetries) {
|
||||
throw lastError;
|
||||
}
|
||||
// Wait before retrying with exponential backoff
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
test.describe("Booking Confirmation and Rejection via API", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
@@ -64,9 +96,14 @@ test.describe("Booking Confirmation and Rejection via API", () => {
|
||||
|
||||
const url = `/api/verify-booking-token?action=accept&token=${oneTimePassword}&bookingUid=${booking.uid}&userId=${organizer.id}`;
|
||||
|
||||
const response = await page.request.get(url, {
|
||||
maxRedirects: 0,
|
||||
});
|
||||
const response = await retryOnNetworkError(
|
||||
() =>
|
||||
page.request.get(url, {
|
||||
maxRedirects: 0,
|
||||
}),
|
||||
3,
|
||||
500
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(303);
|
||||
const location = response.headers()["location"];
|
||||
@@ -133,10 +170,15 @@ test.describe("Booking Confirmation and Rejection via API", () => {
|
||||
|
||||
const url = `/api/verify-booking-token?action=reject&token=${oneTimePassword}&bookingUid=${booking.uid}&userId=${organizer.id}`;
|
||||
|
||||
const response = await page.request.post(url, {
|
||||
data: { reason: "Not available at this time" },
|
||||
maxRedirects: 0,
|
||||
});
|
||||
const response = await retryOnNetworkError(
|
||||
() =>
|
||||
page.request.post(url, {
|
||||
data: { reason: "Not available at this time" },
|
||||
maxRedirects: 0,
|
||||
}),
|
||||
3,
|
||||
500
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(303);
|
||||
const location = response.headers()["location"];
|
||||
|
||||
@@ -47,6 +47,9 @@ test.describe("private links creation and usage", () => {
|
||||
});
|
||||
// book using generated url hash
|
||||
await page.goto($url);
|
||||
await page.waitForURL((url) => {
|
||||
return url.searchParams.get("overlayCalendar") === "true";
|
||||
});
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
// Make sure we're navigated to the success page
|
||||
|
||||
@@ -21,23 +21,27 @@ test.describe("Onboarding", () => {
|
||||
// tests whether the user makes it to /getting-started
|
||||
// after login with completedOnboarding false
|
||||
await page.waitForURL("/getting-started");
|
||||
await expect(page.locator('text="Connect your calendar"')).toBeVisible(); // Fix race condition
|
||||
await expect(page.locator('text="Connect your calendar"').first()).toBeVisible(); // Fix race condition
|
||||
|
||||
await test.step("step 1 - User Settings", async () => {
|
||||
const onboarding = page.getByTestId("onboarding");
|
||||
const form = onboarding.locator("form").first();
|
||||
const submitButton = form.getByTestId("connect-calendar-button");
|
||||
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await submitButton.click();
|
||||
await expect(page.locator("data-testid=required")).toBeVisible();
|
||||
|
||||
// happy path
|
||||
await page.locator("input[name=username]").fill("new user onboarding");
|
||||
await page.locator("input[name=name]").fill("new user 2");
|
||||
await page.locator("input[role=combobox]").click();
|
||||
await form.locator("input[name=username]").fill("new user onboarding");
|
||||
await form.getByLabel("Full name").fill("new user 2");
|
||||
await form.locator("input[role=combobox]").click();
|
||||
await page
|
||||
.locator("*")
|
||||
.filter({ hasText: /^Europe\/London/ })
|
||||
.first()
|
||||
.click();
|
||||
await page.locator("button[type=submit]").click();
|
||||
await submitButton.click();
|
||||
|
||||
await expect(page).toHaveURL(/.*connected-calendar/);
|
||||
|
||||
@@ -71,7 +75,10 @@ test.describe("Onboarding", () => {
|
||||
});
|
||||
|
||||
await test.step("step 5- User Profile", async () => {
|
||||
await page.locator("button[type=submit]").click();
|
||||
const onboarding = page.getByTestId("onboarding");
|
||||
const form = onboarding.locator("form").first();
|
||||
const submitButton = form.getByRole("button", { name: "Finish setup and get started" });
|
||||
await submitButton.click();
|
||||
// should redirect to /event-types after onboarding
|
||||
await page.waitForURL("/event-types");
|
||||
|
||||
|
||||
@@ -38,15 +38,6 @@ test.describe("Organization", () => {
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
orgSlug: org.slug,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
assertInviteLink(inviteLink);
|
||||
await signupFromEmailInviteLink({
|
||||
browser,
|
||||
@@ -112,24 +103,6 @@ test.describe("Organization", () => {
|
||||
// '-domain' because the email doesn't match orgAutoAcceptEmail
|
||||
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
|
||||
await inviteAnEmail(page, invitedUserEmail, true);
|
||||
await expectUserToBeAMemberOfTeam({
|
||||
page,
|
||||
teamId: team.id,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
await expectUserToBeAMemberOfOrganization({
|
||||
page,
|
||||
orgSlug: org.slug,
|
||||
username: usernameDerivedFromEmail,
|
||||
role: "member",
|
||||
isMemberShipAccepted: false,
|
||||
email: invitedUserEmail,
|
||||
});
|
||||
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { createUsersFixture } from "playwright/fixtures/users";
|
||||
import type { createUsersFixture } from "./fixtures/users";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
@@ -53,6 +53,7 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const emailInput = page.getByTestId("profile-form-email-0");
|
||||
|
||||
@@ -219,6 +220,7 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.getByTestId("add-secondary-email").click();
|
||||
|
||||
|
||||
@@ -513,14 +513,13 @@ test.describe("Reschedule Tests", async () => {
|
||||
const orgSlug = org.slug!;
|
||||
const booking = await bookings.create(orgMember.id, orgMember.username, eventType.id);
|
||||
|
||||
const result = await goToUrlWithErrorHandling({ url: `/reschedule/${booking.uid}`, page });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: orgSlug,
|
||||
page,
|
||||
},
|
||||
async ({ page }) => {
|
||||
async ({ page, goToUrlWithErrorHandling }) => {
|
||||
const result = await goToUrlWithErrorHandling(`/reschedule/${booking.uid}`);
|
||||
await page.goto(getNonOrgUrlFromOrgUrl(result.url, orgSlug));
|
||||
await expectSuccessfulReschedule(page, orgSlug);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ const outputDir = path.join(__dirname, "test-results");
|
||||
// So, if not in CI, keep the timers high, if the test is stuck somewhere and there is unnecessary wait developer can see in browser that it's stuck
|
||||
const DEFAULT_NAVIGATION_TIMEOUT = process.env.CI ? 10000 : 120000;
|
||||
const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 10000 : 120000;
|
||||
const DEFAULT_ACTION_TIMEOUT = process.env.CI ? 10000 : 120000;
|
||||
|
||||
// Test Timeout can hit due to slow expect, slow navigation.
|
||||
// So, it should me much higher than sum of expect and navigation timeouts as there can be many async expects and navigations in a single test
|
||||
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 30000 : 240000;
|
||||
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 240000;
|
||||
|
||||
const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS;
|
||||
|
||||
@@ -82,6 +83,8 @@ const DEFAULT_CHROMIUM: NonNullable<PlaywrightTestConfig["projects"]>[number]["u
|
||||
locale: "en-US",
|
||||
/** If navigation takes more than this, then something's wrong, let's fail fast. */
|
||||
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT,
|
||||
/** Global timeout for page actions (click, fill, etc.) on CI */
|
||||
actionTimeout: DEFAULT_ACTION_TIMEOUT,
|
||||
// chromium-specific permissions - Chromium seems to be the only browser type that requires perms
|
||||
contextOptions: {
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
|
||||
Reference in New Issue
Block a user