Skip to content

Copy of PR #22484: feat: optimize E2E test execution speed through strategic consolidation#9

Open
dakshgup wants to merge 1 commit intomainfrom
copy-pr-22484-devin/optimize-e2e-tests-1752505512
Open

Copy of PR #22484: feat: optimize E2E test execution speed through strategic consolidation#9
dakshgup wants to merge 1 commit intomainfrom
copy-pr-22484-devin/optimize-e2e-tests-1752505512

Conversation

@dakshgup
Copy link
Owner

This is a copy of calcom#22484

Originally by: Udit-takkar

PR description is being written. Please check back in a minute.

Devin Session: https://app.devin.ai/sessions/43e00bc6cc03442484a35e383ccac06d


Summary by cubic

Consolidated and cleaned up E2E test files to reduce redundancy, improve execution speed, and lower CI resource usage. Added timing measurement tools and reduced sharding to further optimize test runs.

  • Refactors

    • Merged booking, authentication, and organization tests into fewer, more focused files.
    • Removed 8 redundant or obsolete E2E test files.
    • Reduced E2E shards from 4 to 3 and lowered timeout from 20 to 15 minutes.
  • New Features

    • Added a workflow and script for E2E timing measurement and analysis.

…ugh strategic consolidation

Originally by: Udit-takkar
_PR description is being written. Please check back in a minute._

Devin Session: https://app.devin.ai/sessions/43e00bc6cc03442484a35e383ccac06d

<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Consolidated and cleaned up E2E test files to reduce redundancy, improve execution speed, and lower CI resource usage. Added timing measurement tools and reduced sharding to further optimize test runs.

- **Refactors**
  - Merged booking, authentication, and organization tests into fewer, more focused files.
  - Removed 8 redundant or obsolete E2E test files.
  - Reduced E2E shards from 4 to 3 and lowered timeout from 20 to 15 minutes.

- **New Features**
  - Added a workflow and script for E2E timing measurement and analysis.

<!-- End of auto-generated description by cubic. -->
@github-actions
Copy link

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "Copy of PR #22484: feat: optimize E2E test execution speed through strategic consolidation". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@dakshgup
Copy link
Owner Author

cursor review

1 similar comment
@dakshgup
Copy link
Owner Author

cursor review

cursor[bot]

This comment was marked as outdated.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: E2E Tests Fail Due to Missing User Initialization

Multiple new Playwright E2E tests attempt to retrieve a user from the users fixture using users.get() without first creating any users. This causes the tests to fail as no users exist in the fixture. The tests should use await users.create() to provision a user before attempting to retrieve one.

apps/web/playwright/authentication.e2e.ts#L7-L10

test.describe("Authentication", () => {
test("Should be able to login with email and password", async ({ page, users }) => {
const [user] = users.get();
await page.goto("/auth/login");

apps/web/playwright/organization-consolidated.e2e.ts#L8-L11

test.describe("Organization Management", () => {
test("Should be able to create an organization", async ({ page, users }) => {
const [user] = users.get();
await user.apiLogin();

apps/web/playwright/booking-advanced.e2e.ts#L8-L11

test.describe("Advanced Booking Features", () => {
test("Should handle seat-based bookings", async ({ page, users }) => {
const [user] = users.get();
await user.apiLogin();

apps/web/playwright/booking-core.e2e.ts#L8-L11

test.describe("Core Booking Functionality", () => {
test("Should be able to book a basic event", async ({ page, users }) => {
const [user] = users.get();
await user.apiLogin();

Fix in CursorFix in Web


Bug: Booking Limits Test Coverage Lost

The test.fixme block for Per ${limitUnit} booking limits was accidentally removed, deleting approximately 130 lines of test logic. This eliminated test coverage for booking limits, including reschedule functionality. The forEach loop iterating BOOKING_LIMITS_SINGLE now contains no test logic and is effectively dead code, resulting in a significant loss of test coverage.

apps/web/playwright/booking-limits.e2e.ts#L60-L63

test.describe("Booking limits", () => {
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
const limitUnit = intervalLimitKeyToUnit(limitKey);
});

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR implements a significant optimization of the E2E test suite through strategic consolidation and improved test organization. The changes focus on reducing redundancy and improving execution speed while maintaining comprehensive test coverage.

Key changes include:

  • Consolidation of related tests into focused files (booking-core.e2e.ts, booking-advanced.e2e.ts, authentication.e2e.ts, organization-consolidated.e2e.ts)
  • Removal of 8 redundant test files with functionality merged into consolidated files
  • Infrastructure improvements including reduction from 4 to 3 test shards and timeout reduction from 20 to 15 minutes
  • Addition of timing measurement tools and analysis scripts to guide future optimizations

The consolidation maintains test coverage while reducing CI resource usage and execution time. The new consolidated test files are well-structured with parallel test execution configuration.

Confidence score: 4/5

  1. This PR is safe to merge with careful validation of the consolidated test coverage
  2. Score based on thorough consolidation strategy, maintained test coverage, and addition of timing measurement tools
  3. Files needing attention:
    • booking-core.e2e.ts and booking-advanced.e2e.ts: Verify all critical booking scenarios are covered
    • authentication.e2e.ts: Check 2FA test implementation
    • organization-consolidated.e2e.ts: Ensure all org-specific edge cases are preserved

17 files reviewed, 13 comments
Edit PR Review Bot Settings | Greptile

Comment on lines +38 to +60
test("Should handle organization booking flow", async ({ page, users }) => {
const [user] = users.get();
await user.apiLogin();

await page.goto("/settings/organizations/new");
await page.locator('[name="name"]').fill("Test Organization");
await page.locator('[name="slug"]').fill("test-org");
await page.locator('[data-testid="create-organization"]').click();

await page.goto("/event-types");
await page.locator('[data-testid="new-event-type"]').click();
await page.locator("[name=title]").fill("Org Event");
await page.locator("[name=slug]").fill("org-event");
await page.locator("[data-testid=update-eventtype]").click();

await page.goto("/org/test-org/org-event");
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[name="name"]').fill("John Doe");
await page.locator('[name="email"]').fill("john@example.com");
await page.locator('[data-testid="confirm-book-button"]').click();

await expect(page.locator('[data-testid="success-page"]')).toBeVisible();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Organization booking flow test creates a new org for each test. Consider using beforeAll() to create a single org for all booking-related tests to reduce test execution time.

Comment on lines +76 to +91
test("Should require 2FA code when enabled", async ({ page, users }) => {
const [user] = users.get();
await user.apiLogin();

await page.goto("/settings/security/two-factor-auth");
await page.locator('[data-testid="enable-2fa-button"]').click();

await page.goto("/auth/logout");
await page.goto("/auth/login");

await page.locator('[name="email"]').fill(user.email);
await page.locator('[name="password"]').fill(user.username || "password");
await page.locator('[type="submit"]').click();

await expect(page.locator('[data-testid="2fa-input"]')).toBeVisible();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Test needs cleanup after 2FA enablement - enabled 2FA could affect other tests. Consider adding afterEach hook to disable 2FA

await page.locator("[name=slug]").fill("event-with-questions");
await page.locator("[data-testid=update-eventtype]").click();

const eventTypeId = await page.locator("[data-testid=update-eventtype]").getAttribute("data-id");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: getAttribute('data-id') could return null. Add null check before using eventTypeId.

Suggested change
const eventTypeId = await page.locator("[data-testid=update-eventtype]").getAttribute("data-id");
const eventTypeId = await page.locator("[data-testid=update-eventtype]").getAttribute("data-id");
if (!eventTypeId) {
throw new Error("Event type ID not found");
}

Comment on lines 61 to 63
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
const limitUnit = intervalLimitKeyToUnit(limitKey);

// test one limit at a time
test.fixme(`Per ${limitUnit}`, async ({ page, users }) => {
const slug = `booking-limit-${limitUnit}`;
const singleLimit = { [limitKey]: bookingLimit };

const user = await createUserWithLimits({
users,
slug,
length: EVENT_LENGTH,
bookingLimits: singleLimit,
});

let slotUrl = "";

const monthUrl = getLastEventUrlWithMonth(user, firstMondayInBookingMonth);
await page.goto(monthUrl);

const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
const bookingDay = availableDays.getByText(firstMondayInBookingMonth.date().toString(), {
exact: true,
});

// finish rendering days before counting
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
const availableDaysBefore = await availableDays.count();

let latestRescheduleUrl: string | null = null;
await test.step("can book up to limit", async () => {
for (let i = 0; i < bookingLimit; i++) {
await bookingDay.click();

await page.getByTestId("time").nth(0).click();
await bookTimeSlot(page);

slotUrl = page.url();

await expect(page.getByTestId("success-page")).toBeVisible();
latestRescheduleUrl = await page
.locator('span[data-testid="reschedule-link"] > a')
.getAttribute("href");

await page.goto(monthUrl);
}
});

const expectedAvailableDays = {
day: -1,
week: -5,
month: 0,
year: 0,
};

await test.step("but not over", async () => {
// should already have navigated to monthUrl - just ensure days are rendered
await expect(page.getByTestId("day").nth(0)).toBeVisible();

// ensure the day we just booked is now blocked
await expect(bookingDay).toBeHidden({ timeout: 10_000 });

const availableDaysAfter = await availableDays.count();

// equals 0 if no available days, otherwise signed difference
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
expectedAvailableDays[limitUnit]
);

// try to book directly via form page
await page.goto(slotUrl);
await expectSlotNotAllowedToBook(page);
});

await test.step("but can reschedule", async () => {
const bookingId = latestRescheduleUrl?.split("/").pop();
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: bookingId } });

const year = rescheduledBooking.startTime.getFullYear();
const month = String(rescheduledBooking.startTime.getMonth() + 1).padStart(2, "0");
const day = String(rescheduledBooking.startTime.getDate()).padStart(2, "0");

await page.goto(
`/${user.username}/${
user.eventTypes.at(-1)?.slug
}?rescheduleUid=${bookingId}&date=${year}-${month}-${day}&month=${year}-${month}`
);

const formerDay = availableDays.getByText(rescheduledBooking.startTime.getDate().toString(), {
exact: true,
});
await expect(formerDay).toBeVisible();

const formerTimeElement = page.locator('[data-testid="former_time_p"]');
await expect(formerTimeElement).toBeVisible();

await page.locator('[data-testid="time"]').nth(0).click();

await expect(page.locator('[name="name"]')).toBeDisabled();
await expect(page.locator('[name="email"]')).toBeDisabled();

await confirmReschedule(page);

await expect(page.locator("[data-testid=success-page]")).toBeVisible();

const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: bookingId } });
expect(newBooking).not.toBeNull();

const updatedRescheduledBooking = await prisma.booking.findFirstOrThrow({
where: { uid: bookingId },
});
expect(updatedRescheduledBooking.status).toBe(BookingStatus.CANCELLED);

await prisma.booking.deleteMany({
where: {
id: {
in: [newBooking.id, rescheduledBooking.id],
},
},
});
});

await test.step(`month after booking`, async () => {
await page.goto(getLastEventUrlWithMonth(user, firstMondayInBookingMonth.add(1, "month")));

// finish rendering days before counting
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });

// the month after we made bookings should have availability unless we hit a yearly limit
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
});
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Empty test block should be removed completely rather than leaving placeholder. This forEach loop now does nothing.

Comment on lines +86 to +87
await page.locator('[name="email"]').fill(user.email);
await page.locator('[name="password"]').fill(user.username || "password");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Using username as password fallback is unsafe. Should use a dedicated test password

await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[name="name"]').fill("John Doe");
await page.locator('[name="email"]').fill("john@example.com");
await page.locator('[name="responses.Select your preference"]').selectOption("Option 1");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This selector may be fragile - use a more specific data-testid for form responses instead of relying on the question text in the selector

const eventTypeId = await page.locator("[data-testid=update-eventtype]").getAttribute("data-id");
await page.goto(`/event-types/${eventTypeId}?tabName=limits`);

await page.locator('input[name="durationLimits.PER_DAY"]').fill("60");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Consider adding a validation check for the duration limit being respected - currently only verifies the success page

Comment on lines +100 to +105
if: always()
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - E2E_START_TIME))
echo "E2E execution time for shard ${{ matrix.shard }}: ${DURATION} seconds"
echo "SHARD_DURATION=${DURATION}" >> $GITHUB_ENV
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: SHARD_DURATION is set but never used. Remove if not needed.

Suggested change
if: always()
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - E2E_START_TIME))
echo "E2E execution time for shard ${{ matrix.shard }}: ${DURATION} seconds"
echo "SHARD_DURATION=${DURATION}" >> $GITHUB_ENV
if: always()
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - E2E_START_TIME))
echo "E2E execution time for shard ${{ matrix.shard }}: ${DURATION} seconds"

Comment on lines +16 to +32
async loadTimingData() {
try {
const files = fs.readdirSync(this.timingDataPath);

for (const file of files) {
if (file.endsWith('-timing.json')) {
const filePath = path.join(this.timingDataPath, file);
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
this.shardData.push(data);
}
}

console.log(`Loaded timing data for ${this.shardData.length} shards`);
} catch (error) {
console.error('Error loading timing data:', error.message);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing proper error propagation - should throw or set error state

console.log(` Estimated time saving: ${rec.estimatedTimeSaving}`);
});

fs.writeFileSync('e2e-optimization-report.json', JSON.stringify(report, null, 2));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Add try/catch around file write operation

Suggested change
fs.writeFileSync('e2e-optimization-report.json', JSON.stringify(report, null, 2));
try {
fs.writeFileSync('e2e-optimization-report.json', JSON.stringify(report, null, 2));
console.log('\n📄 Detailed report saved to: e2e-optimization-report.json');
} catch (error) {
console.error('Error saving report:', error.message);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant