Scheduling Integration Testing Guide

How to Test Calendly Booking Flows with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Calendly booking with Playwright. Standalone pages, embedded iframes, timezone detection, custom intake forms, availability edge cases, and the cross-origin pitfalls that silently break scheduling tests.

10M+

Calendly powers over 10 million meetings per month across hundreds of thousands of organizations, making it the most widely embedded scheduling widget on the web.

0 stepsBooking Steps
0 scenariosTest Scenarios
0 iframeiframe + cross-origin
0 zonesTimezone Handling

1. Why Testing Calendly Booking Is Complex

Calendly looks simple from the outside. A user picks a date, picks a time, fills in their name and email, and confirms. But under the surface, Calendly is a multi-step, cross-origin, timezone-aware scheduling engine that presents several structural challenges for automated testing.

First, when Calendly is embedded in your application, the entire booking widget runs inside an iframe hosted on calendly.com. Your Playwright tests cannot interact with it using ordinary page locators. You need to cross a frame boundary, and the iframe content is controlled by Calendly, not you. Second, the booking flow is inherently multi-step: select a date from the calendar, select an available time slot, fill in contact details and any custom questions, then confirm. Each step triggers a re-render inside the iframe, and the transitions between steps have variable loading times depending on Calendly's API response speed.

Third, timezone handling is a constant source of subtle bugs. Calendly detects the visitor's timezone via the browser and displays available slots in that local time. If your CI server runs in UTC but your Calendly account is configured in US Eastern, the time slots your test sees will differ from what a developer sees locally. Fourth, Calendly supports custom intake questions (text fields, dropdowns, radio buttons, phone numbers) that vary per event type, so your test must handle dynamic form structures. Fifth, the date picker disables past dates and dates with no availability, which means your test cannot hard-code a specific date without risking a failure when that date passes or fills up.

A reliable Calendly test suite accounts for all five of these surfaces. The sections below walk through each scenario with runnable Playwright TypeScript code and equivalent Assrt plain-English descriptions.

Standalone Calendly Booking Flow

🌐

Open Calendly link

Select date

Calendar picker

Select time slot

Available times

📧

Fill details

Name, email, questions

Confirm booking

↪️

Redirect or confirmation

Embedded Iframe Booking Flow

🌐

Load your page

📦

Calendly iframe loads

Cross-origin

Select date in iframe

Select time in iframe

📧

Fill form in iframe

Confirm in iframe

🔔

Parent page callback

postMessage event

Calendly Booking Sequence

BrowserYour PageCalendly iframeCalendly APINavigate to pageLoad embed widgetFetch availabilityAvailable slotsSelect dateSelect time slotFill form detailsCreate bookingBooking confirmedpostMessage: event_scheduledRedirect to thank-you

2. Setting Up Your Test Environment

Before you write a single test, configure a dedicated Calendly account and event type for testing. Using your production Calendly account will create real bookings on your calendar and pollute your scheduling data. Create a separate Calendly account with a test event type that has predictable availability windows.

Test Event Type Configuration

Create a 30-minute event type in your test account with the following settings. Set availability to every weekday, 9:00 AM to 5:00 PM, in a fixed timezone (for example, America/New_York). Add at least one custom question (a text field asking "Company name") so you can test intake form handling. Enable the redirect on confirmation to a URL you control. Disable email notifications to avoid inbox clutter during test runs.

Calendly Test Account Setup Checklist

  • Create a dedicated Calendly account for testing (separate from production)
  • Create a 30-minute event type with weekday availability, 9 AM to 5 PM
  • Set a fixed timezone (e.g. America/New_York) on the event type
  • Add at least one custom intake question (text field: "Company name")
  • Configure a post-booking redirect URL to a page you control
  • Disable email notifications to avoid inbox clutter during test runs
  • Create a second event type with zero availability for edge case tests
  • Generate a Calendly API token for test cleanup and teardown

Environment Variables

.env.test

Playwright Configuration for Cross-Origin Iframes

Calendly iframes are served from calendly.com, which is a different origin than your application. Playwright handles cross-origin iframes out of the box, but you need to be aware that the iframe content loads asynchronously. Do not attempt to interact with elements inside the Calendly iframe until the iframe has finished loading its initial view.

playwright.config.ts

Helper Utilities

Because Calendly tests depend on selecting future available dates, you need a utility that finds the next available weekday. Hard-coding a date like "April 15" will fail once that date passes.

// test-utils/calendly-helpers.ts
export function getNextWeekday(): Date {
  const date = new Date();
  date.setDate(date.getDate() + 1);
  while (date.getDay() === 0 || date.getDay() === 6) {
    date.setDate(date.getDate() + 1);
  }
  return date;
}

export function formatCalendlyDate(date: Date): string {
  return date.toLocaleDateString('en-US', {
    weekday: 'long',
    month: 'long',
    day: 'numeric',
  });
}

3. Scenario: Standalone Booking Happy Path

1

Standalone Booking: Date, Time, Details, Confirm

Moderate

The standalone flow is when the user navigates directly to a calendly.com link (for example, calendly.com/your-org/30-minute-meeting). This is the simplest Calendly test because there is no iframe boundary to cross. The entire page is the Calendly application.

Goal

Navigate to the Calendly event page, select the next available date, pick the first available time slot, fill in name and email, and confirm the booking. Assert the confirmation screen is displayed.

Playwright Implementation

calendly-booking.spec.ts

Key Details

The calendar renders date cells as buttons. Disabled buttons represent past dates or dates with no availability. By selecting the first enabled button, you avoid hard-coding a specific date. The time slot buttons appear only after a date is selected, so always wait for them to become visible before clicking. The confirmation screen includes the invitee email, which serves as your primary assertion anchor.

Standalone Booking: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('standalone booking: happy path', async ({ page }) => {
  const email = `test+${Date.now()}@assrt.ai`;
  await page.goto(process.env.CALENDLY_EVENT_URL!);
  await page.waitForSelector('[data-container="calendar"]', {
    timeout: 15_000,
  });
  const availableDate = page.locator(
    'button[data-container="date-cell"]:not([disabled])'
  ).first();
  await availableDate.click();
  const timeSlot = page.getByTestId('time-button').first();
  await expect(timeSlot).toBeVisible({ timeout: 10_000 });
  await timeSlot.click();
  await page.getByRole('button', { name: /next/i }).click();
  await page.getByLabel(/^name/i).fill('Jane Tester');
  await page.getByLabel(/email/i).fill(email);
  await page.getByRole('button', { name: /schedule event/i }).click();
  await expect(
    page.getByRole('heading', { name: /confirmed/i })
  ).toBeVisible({ timeout: 15_000 });
  await expect(page.getByText(email)).toBeVisible();
});
63% fewer lines

4. Scenario: Embedded Iframe Booking on Your Page

2

Embedded Calendly Widget Inside Your Application

Complex

When you embed Calendly on your own site using their inline embed script, the entire booking widget runs inside an iframe from calendly.com. This is the most common production setup and also the hardest to test. Every interaction must go through Playwright's frameLocator to reach inside the cross-origin iframe.

The Embed Structure

Calendly's inline embed injects a <div class="calendly-inline-widget"> containing an iframe whose src points to calendly.com. The iframe loads the same scheduling UI you see on the standalone page, but now it is sandboxed inside your page's DOM. Playwright's frameLocator lets you target elements inside this iframe by matching on the iframe's src attribute.

Playwright Implementation

calendly-booking.spec.ts

Listening for the Parent Page Callback

Calendly's embed SDK fires postMessage events to the parent window when certain actions happen. The most important event is calendly.event_scheduled, which fires after a successful booking. You can listen for this event in your test to verify the parent page received the callback.

// After navigating to the embed page, set up a listener
const scheduledEvent = page.evaluate(() => {
  return new Promise<any>((resolve) => {
    window.addEventListener('message', (e) => {
      if (e.data?.event === 'calendly.event_scheduled') {
        resolve(e.data);
      }
    });
  });
});

// ... complete the booking flow ...

// After confirming, verify the postMessage was received
const eventData = await scheduledEvent;
expect(eventData.event).toBe('calendly.event_scheduled');
expect(eventData.payload.invitee.uri).toBeTruthy();

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Custom Intake Form Fields

3

Booking with Custom Questions and Required Fields

Moderate

Calendly event types can include custom questions: text inputs, multi-line text areas, radio buttons, dropdowns, checkboxes, and phone number fields. These appear on the details step alongside the standard name and email fields. If your event type requires a phone number or a custom "Company name" field, the booking will fail unless your test fills them in. This is a frequent source of test failures when someone adds a required question to the event type without updating the test.

Playwright Implementation

test('booking with custom intake form fields', async ({ page }) => {
  const email = `intake+${Date.now()}@assrt.ai`;

  await page.goto(process.env.CALENDLY_EVENT_URL!);

  // Select date and time (same pattern as happy path)
  const availableDate = page.locator(
    'button[data-container="date-cell"]:not([disabled])'
  ).first();
  await availableDate.click();
  await page.getByTestId('time-button').first().click();
  await page.getByRole('button', { name: /next/i }).click();

  // Fill standard fields
  await page.getByLabel(/^name/i).fill('Intake Tester');
  await page.getByLabel(/email/i).fill(email);

  // Fill custom text question
  await page.getByLabel(/company name/i).fill('Assrt Inc.');

  // Fill custom phone number field (if configured)
  const phoneField = page.getByLabel(/phone/i);
  if (await phoneField.isVisible()) {
    await phoneField.fill('+1 555 123 4567');
  }

  // Fill custom dropdown (if configured)
  const dropdown = page.getByLabel(/how did you hear/i);
  if (await dropdown.isVisible()) {
    await dropdown.selectOption({ label: 'Search engine' });
  }

  // Fill custom radio button (if configured)
  const radio = page.getByLabel(/team size/i);
  if (await radio.first().isVisible()) {
    await page.getByLabel('1-10').check();
  }

  // Submit
  await page.getByRole('button', { name: /schedule event/i }).click();

  // Assert confirmation
  await expect(
    page.getByRole('heading', { name: /confirmed/i })
  ).toBeVisible({ timeout: 15_000 });
});

Handling Validation Errors

If you try to submit without filling a required custom field, Calendly displays inline validation errors. A good defensive test submits the form with a required field left blank, asserts the validation error appears, fills the field, then submits again. This verifies that your event type's validation configuration actually works and that users are not silently blocked from booking.

6. Scenario: Timezone Handling and Verification

Timezone mismatches are one of the most common production bugs in scheduling integrations. A user in Tokyo books what they think is a 2:00 PM slot, but the host sees it as 1:00 AM because the timezone conversion went wrong somewhere. Calendly handles this correctly on its side, but your integration may not. Your test needs to verify that the timezone displayed in the booking widget matches expectations and that the confirmed time is what the user intended.

4

Timezone Detection and Display Verification

Complex

Playwright Implementation

test.describe('timezone handling', () => {
  test('displays times in the browser timezone', async ({ browser }) => {
    // Create a context with a specific timezone
    const context = await browser.newContext({
      timezoneId: 'Asia/Tokyo',
      locale: 'en-US',
    });
    const page = await context.newPage();

    await page.goto(process.env.CALENDLY_EVENT_URL!);

    // Wait for the calendar to load
    await page.waitForSelector('[data-container="calendar"]', {
      timeout: 15_000,
    });

    // Verify the timezone indicator shows Japan Standard Time
    const tzLabel = page.locator('[data-container="timezone"]');
    await expect(tzLabel).toContainText(/Japan|JST|Tokyo/i);

    // Select a date and verify time slots are in JST range
    const availableDate = page.locator(
      'button[data-container="date-cell"]:not([disabled])'
    ).first();
    await availableDate.click();

    const firstTimeSlot = page.getByTestId('time-button').first();
    await expect(firstTimeSlot).toBeVisible({ timeout: 10_000 });

    // Time should be displayed in Japan timezone format
    const timeText = await firstTimeSlot.textContent();
    expect(timeText).toBeTruthy();
    // Calendly shows times like "9:00am" or "2:30pm"
    expect(timeText).toMatch(/\d{1,2}:\d{2}(am|pm)/i);

    await context.close();
  });

  test('times differ when timezone changes', async ({ browser }) => {
    // Book with US Eastern timezone
    const eastContext = await browser.newContext({
      timezoneId: 'America/New_York',
      locale: 'en-US',
    });
    const eastPage = await eastContext.newPage();
    await eastPage.goto(process.env.CALENDLY_EVENT_URL!);
    await eastPage.waitForSelector('[data-container="calendar"]');

    const eastDate = eastPage.locator(
      'button[data-container="date-cell"]:not([disabled])'
    ).first();
    await eastDate.click();
    const eastTime = await eastPage
      .getByTestId('time-button')
      .first()
      .textContent();

    // Now check the same event with Pacific timezone
    const westContext = await browser.newContext({
      timezoneId: 'America/Los_Angeles',
      locale: 'en-US',
    });
    const westPage = await westContext.newPage();
    await westPage.goto(process.env.CALENDLY_EVENT_URL!);
    await westPage.waitForSelector('[data-container="calendar"]');

    const westDate = westPage.locator(
      'button[data-container="date-cell"]:not([disabled])'
    ).first();
    await westDate.click();
    const westTime = await westPage
      .getByTestId('time-button')
      .first()
      .textContent();

    // The displayed times should differ by 3 hours
    // (Eastern is UTC-5, Pacific is UTC-8 during standard time)
    expect(eastTime).not.toBe(westTime);

    await eastContext.close();
    await westContext.close();
  });
});

The key insight is that Playwright's timezoneId context option controls what Intl.DateTimeFormat returns inside the browser. Calendly reads this to detect the visitor's timezone. By creating two browser contexts with different timezones and loading the same event page, you can verify that Calendly correctly adjusts the displayed time slots. This test catches the bug where your embed page overrides or ignores the browser timezone.

7. Scenario: No Availability State

5

Fully Booked Date and No Availability Handling

Moderate

When all time slots for a given date are booked, or when the event type has no availability for an entire month, Calendly displays an empty state. Your application needs to handle this gracefully. If your embed page shows a loading spinner indefinitely because it never receives a calendly.event_scheduled callback, that is a bug your test should catch.

Testing with a Fully Booked Event Type

The cleanest approach is to create a second event type in your test Calendly account with zero availability. Set its schedule to have no open hours on any day. When your test navigates to that event type, Calendly will display the "no times available" message.

test('no availability: shows empty state message', async ({ page }) => {
  // Use an event type configured with zero availability
  await page.goto(
    'https://calendly.com/test-account/no-availability-event'
  );

  // Wait for the calendar to load
  await page.waitForSelector('[data-container="calendar"]', {
    timeout: 15_000,
  });

  // Assert there are no enabled date buttons
  const enabledDates = page.locator(
    'button[data-container="date-cell"]:not([disabled])'
  );
  await expect(enabledDates).toHaveCount(0, { timeout: 10_000 });

  // Calendly shows a message when no times are available
  await expect(
    page.getByText(/no times available/i)
  ).toBeVisible();
});

test('no availability: embedded widget shows fallback UI', async ({ page }) => {
  // Navigate to your embed page pointing at the no-availability event
  await page.goto('/schedule-unavailable');

  const calendlyFrame = page.frameLocator(
    'iframe[src*="calendly.com"]'
  );

  // Verify the iframe loaded
  await expect(
    calendlyFrame.locator('[data-container="calendar"]')
  ).toBeVisible({ timeout: 20_000 });

  // Assert no enabled dates
  const enabledDates = calendlyFrame.locator(
    'button[data-container="date-cell"]:not([disabled])'
  );
  await expect(enabledDates).toHaveCount(0, { timeout: 10_000 });

  // Your parent page should display a fallback message or
  // contact link when Calendly has no availability
  await expect(
    page.getByText(/contact us directly/i)
  ).toBeVisible();
});

Testing a Single Fully Booked Day

To test the case where a specific date has no remaining slots (rather than the entire event type having zero availability), you can use the Calendly API to pre-book all slots for a target date in your test setup, then verify that clicking that date shows no available times. This requires API access and should be part of your test setup and teardown, canceling the bookings after the test completes.

8. Scenario: Post-Booking Redirect Verification

6

Redirect After Booking Confirmation

Straightforward

Calendly supports configuring a redirect URL that the user is sent to after confirming a booking. This is commonly used to redirect to a thank-you page, an onboarding flow, or a page that captures additional information. When the event type has a redirect URL configured, Calendly navigates the browser (or the parent frame) to that URL after the booking confirmation screen. Your test needs to verify that the redirect actually fires and that the destination page loads correctly.

Standalone Redirect

test('post-booking redirect to thank-you page', async ({ page }) => {
  const email = `redirect+${Date.now()}@assrt.ai`;

  await page.goto(process.env.CALENDLY_EVENT_URL!);

  // Complete the booking flow
  const availableDate = page.locator(
    'button[data-container="date-cell"]:not([disabled])'
  ).first();
  await availableDate.click();
  await page.getByTestId('time-button').first().click();
  await page.getByRole('button', { name: /next/i }).click();
  await page.getByLabel(/^name/i).fill('Redirect Tester');
  await page.getByLabel(/email/i).fill(email);
  await page.getByRole('button', { name: /schedule event/i }).click();

  // Wait for the confirmation screen first
  await expect(
    page.getByRole('heading', { name: /confirmed/i })
  ).toBeVisible({ timeout: 15_000 });

  // Then wait for the redirect to your configured URL
  await page.waitForURL(/\/booking-confirmed/, { timeout: 30_000 });

  // Assert the thank-you page loaded correctly
  await expect(
    page.getByRole('heading', { name: /thank you/i })
  ).toBeVisible();

  // Verify query parameters passed by Calendly
  const url = new URL(page.url());
  expect(url.searchParams.get('invitee_email')).toBe(email);
});

Embedded Redirect

When Calendly is embedded, the redirect behavior depends on how the embed is configured. By default, the redirect navigates the top-level page (your application), not just the iframe. If your embed uses the parentDomain parameter, you can also listen for the calendly.event_scheduled postMessage and handle the redirect in your own JavaScript. Test both paths to make sure one or the other fires reliably.

test('embedded redirect navigates parent page', async ({ page }) => {
  const email = `embed-redirect+${Date.now()}@assrt.ai`;

  await page.goto('/schedule');
  const calendlyFrame = page.frameLocator('iframe[src*="calendly.com"]');

  // Complete booking inside iframe
  await expect(
    calendlyFrame.locator('[data-container="calendar"]')
  ).toBeVisible({ timeout: 20_000 });
  await calendlyFrame.locator(
    'button[data-container="date-cell"]:not([disabled])'
  ).first().click();
  await calendlyFrame.getByTestId('time-button').first().click();
  await calendlyFrame.getByRole('button', { name: /next/i }).click();
  await calendlyFrame.getByLabel(/^name/i).fill('Embed Redirect');
  await calendlyFrame.getByLabel(/email/i).fill(email);
  await calendlyFrame
    .getByRole('button', { name: /schedule event/i })
    .click();

  // The redirect should navigate the parent page
  await page.waitForURL(/\/booking-confirmed/, { timeout: 30_000 });
  await expect(
    page.getByRole('heading', { name: /thank you/i })
  ).toBeVisible();
});

9. Common Pitfalls That Break Calendly Test Suites

Hard-Coded Dates

The single most common failure in Calendly tests is a hard-coded date. A test that clicks "April 15" will pass today and fail next week when April 15 becomes a past date and Calendly disables the button. Always select dates dynamically by finding the first enabled date cell. If you must target a specific day of the week, compute it from today's date.

Iframe Selector Fragility

Calendly updates its embed widget periodically. The internal DOM structure, class names, and data attributes can change without notice. Using frameLocator('iframe[src*="calendly.com"]') to find the iframe is stable because the src domain will not change. But inside the iframe, prefer getByRole, getByLabel, and getByTestId over CSS class selectors. Calendly provides data-container attributes on key elements, which are more stable than class names.

Race Conditions on Calendar Load

The Calendly calendar fetches availability data from its API after the initial page render. If your test clicks a date cell before the availability data has loaded, the click may land on a cell that becomes disabled a moment later. Always wait for the [data-container="calendar"] element to be visible and then assert the target date cell is not disabled before clicking.

Timezone Drift in CI

CI servers typically run in UTC. If your test expects to see a 9:00 AM slot but the event type's availability is 9:00 AM to 5:00 PM Eastern, the UTC visitor will see times starting at 2:00 PM. Worse, if it is currently 6:00 PM Eastern (11:00 PM UTC), the CI server may see zero availability for the current day. Always set timezoneId in your Playwright config or browser context to a timezone that matches your Calendly account's availability window.

Event Cleanup

Every successful test creates a real booking in your Calendly test account. These accumulate over time and can exhaust your availability slots, causing subsequent tests to fail because no times remain. Add a teardown step that cancels bookings created by the test run using the Calendly API. Tag test bookings with a recognizable email pattern like `test+${timestamp}@assrt.ai` so you can identify and cancel them programmatically.

// global-teardown.ts
import { request } from '@playwright/test';

export default async function globalTeardown() {
  const api = await request.newContext({
    baseURL: 'https://api.calendly.com',
    extraHTTPHeaders: {
      Authorization: `Bearer ${process.env.CALENDLY_API_TOKEN}`,
    },
  });

  // List recent scheduled events
  const res = await api.get('/scheduled_events', {
    params: {
      user: process.env.CALENDLY_USER_URI!,
      min_start_time: new Date(
        Date.now() - 24 * 60 * 60 * 1000
      ).toISOString(),
      status: 'active',
    },
  });
  const { collection } = await res.json();

  // Cancel events created by test email addresses
  for (const event of collection) {
    const inviteesRes = await api.get(
      `${event.uri}/invitees`
    );
    const invitees = await inviteesRes.json();
    const isTestBooking = invitees.collection.some(
      (inv: any) => inv.email.includes('@assrt.ai')
    );
    if (isTestBooking) {
      await api.post(`${event.uri}/cancellation`, {
        data: { reason: 'Automated test cleanup' },
      });
    }
  }
}

Flaky Date Navigation

When the current month has no availability (for example, all remaining days are weekends), Calendly automatically navigates to the next month. But if your test tries to click a date before the month navigation animation completes, the click can land on the wrong element. Wait for the calendar to settle by asserting the presence of an enabled date cell before interacting with it. A short toBeVisible assertion with a timeout is more reliable than any fixed delay.

Calendly Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above is 30 to 60 lines of Playwright TypeScript. Multiply that by the six or more scenarios you actually need and you have hundreds of lines of test code that will break the first time Calendly renames a data attribute or restructures their calendar component. Assrt lets you describe the scenario in plain English, generates the equivalent Playwright code, and regenerates the selectors automatically when the underlying widget changes.

The standalone happy path from Section 3 looks like this in Assrt:

# scenarios/calendly-standalone-booking.assrt
describe: Happy path standalone Calendly booking

given:
  - I am on the Calendly event page
  - I use a fresh random email

steps:
  - wait for the calendar to load
  - click the first available date
  - click the first available time slot
  - click "Next"
  - fill in name with "Jane Tester"
  - fill in email with my random email
  - click "Schedule Event"

expect:
  - the confirmation heading is visible within 15 seconds
  - my email is displayed on the confirmation screen

The embedded iframe scenario from Section 4:

# scenarios/calendly-embedded-booking.assrt
describe: Embedded Calendly booking inside our schedule page

given:
  - I am on the /schedule page
  - the Calendly widget has loaded inside the iframe

steps:
  - inside the Calendly iframe, click the first available date
  - inside the Calendly iframe, click the first available time slot
  - inside the Calendly iframe, click "Next"
  - inside the Calendly iframe, fill name with "Embed Tester"
  - inside the Calendly iframe, fill email with a fresh random email
  - inside the Calendly iframe, click "Schedule Event"

expect:
  - the confirmation heading appears inside the iframe
  - the parent page receives a calendly.event_scheduled message

The timezone verification scenario from Section 6:

# scenarios/calendly-timezone.assrt
describe: Calendly shows correct times for visitor timezone

given:
  - my browser timezone is set to Asia/Tokyo

steps:
  - go to the Calendly event page
  - wait for the calendar to load

expect:
  - the timezone label shows "Japan" or "JST" or "Tokyo"
  - the first available time slot shows a valid time format

Assrt compiles each of these files into the Playwright TypeScript code you saw in the earlier sections, committed to your repo as real tests you can read, run, and modify. When Calendly updates their DOM structure or renames an internal attribute, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated locators. Your plain-English scenario file stays unchanged.

Start with the standalone happy path. Once it is green in your CI, add the embedded iframe scenario, then timezone verification, then the intake form test, then the no-availability edge case, then the redirect flow. In a single afternoon you can build complete Calendly booking coverage that would take a week to write and maintain by hand.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk