Scheduling & Embed Testing Guide

How to Test Acuity Scheduling Embed with Playwright: Iframes, Intake Forms, and Timezone Pitfalls

A scenario-by-scenario walkthrough of testing Acuity Scheduling embeds with Playwright. Covers iframe access, appointment type selection, intake form validation, timezone conversion, calendar date picking, cancellation flows, and the silent failures that break scheduling tests in CI.

500M+

β€œSquarespace (which owns Acuity Scheduling) reported over 500 million unique scheduling sessions processed through its platform in 2025, making Acuity one of the most widely embedded third-party schedulers on the web.”

Squarespace 2025 Investor Report

0+Cross-origin iframe layers
0Scheduling scenarios covered
0Intake form field types
0%Fewer lines with Assrt

Acuity Scheduling Embed Booking Flow

User BrowserYour WebsiteAcuity IframeAcuity APIConfirmation EmailVisit booking pageLoad embed iframeGET /schedule.php?owner=...Render appointment typesSelect type, date, timePOST intake form dataBooking confirmedSend confirmation emailShow confirmation page

1. Why Testing Acuity Scheduling Embeds Is Harder Than It Looks

Acuity Scheduling is a third-party scheduling tool (owned by Squarespace) that most businesses embed on their websites using an iframe. The iframe loads content from acuityscheduling.com on a completely different origin than your host page. This cross-origin boundary is the first and most persistent obstacle. Playwright cannot use standard page.locator() calls to reach elements inside the iframe. You must use page.frameLocator() to pierce the iframe boundary, and every subsequent interaction must chain off that frame locator.

The complexity compounds because Acuity's embed is not a static form. It is a multi-step wizard: first you choose an appointment type, then a date, then a time slot, then you fill out intake fields (name, email, phone, plus any custom fields the business configures), and finally you confirm. Each step triggers an asynchronous request to Acuity's API that fetches new content and replaces the iframe's DOM. Standard Playwright waits like waitForSelector can fire prematurely if you are not waiting for the correct network idle or the specific element that signals the next step has fully loaded.

There are five structural reasons this flow breaks automated tests. First, the cross-origin iframe means your selectors must always go through a frame locator, and any accidental use of page.locator()returns zero matches silently. Second, the multi-step wizard has transition animations that cause timing issues. Third, Acuity's date picker uses a custom calendar widget with dynamically generated class names, not standard HTML date inputs. Fourth, timezone handling is automatic based on the browser's locale, meaning the same test can select different time slots in CI versus local development. Fifth, custom intake fields are business-configurable, so the form structure can change without any code deploy on your side.

Acuity Embed Multi-Step Booking Flow

🌐

Your Page

Loads Acuity iframe

βš™οΈ

Appointment Type

User selects service

🌐

Calendar

Pick date from widget

βš™οΈ

Time Slot

Available times load

πŸ“§

Intake Form

Name, email, custom fields

βœ…

Confirmation

Booking created

Iframe Communication Architecture

🌐

Host Page

yoursite.com

πŸ”’

Iframe Boundary

Cross-origin sandbox

πŸ“¦

Acuity Embed

acuityscheduling.com

βš™οΈ

Acuity API

REST endpoints

πŸ””

Webhook

Booking callback

A reliable Acuity Scheduling test suite must handle all five of these obstacles. The sections below walk through each scenario with runnable Playwright TypeScript code that you can adapt to your specific Acuity configuration.

2. Setting Up a Reliable Test Environment

Acuity Scheduling offers a sandbox mode via its API, but the embed itself always loads from production Acuity servers. Unlike Stripe or Auth0 where you can spin up a dedicated test tenant, Acuity tests run against your real scheduling account. This means every test booking creates a real appointment that you must clean up afterward. The Acuity API (v1) supports programmatic cancellation, which is essential for teardown.

Acuity Test Environment Setup Checklist

  • Create a dedicated Acuity account or use a test calendar within your existing account
  • Generate an API key from Acuity > Integrations > API for programmatic cleanup
  • Create a test appointment type with known intake fields (name, email, phone, one custom field)
  • Set the test appointment type availability to a wide window to avoid flaky date selection
  • Configure a webhook endpoint or use the API to verify bookings in tests
  • Note your Acuity owner ID (the numeric ID in the embed URL)
  • Set the embed timezone to a fixed value for deterministic tests
  • Disable CAPTCHA on the test appointment type if available

Environment Variables

.env.test

API Helper for Test Cleanup

Every test that creates a booking must cancel it in teardown. The Acuity API v1 uses basic authentication with your user ID and API key. Create a helper that cancels appointments by ID so your afterEach hooks can clean up reliably.

test/helpers/acuity-api.ts

Playwright Configuration for Iframe Embeds

Acuity embeds require extra navigation timeout because the iframe loads asynchronously after the host page renders. Set your Playwright config to allow generous timeouts and configure a fixed timezone via the locale settings to eliminate timezone flakiness.

playwright.config.ts
Install Dependencies

3. Scenario: Accessing the Acuity Iframe and Selecting an Appointment Type

The very first challenge is reliably locating and interacting with the Acuity embed iframe. Acuity embeds use an <iframe> tag with a src pointing to app.acuityscheduling.com/schedule.php. The iframe may be nested inside wrapper divs, and some website builders add their own iframe layers. Your test needs to find the correct frame, wait for it to fully load (not just the iframe tag, but the content inside it), and then select the desired appointment type from the list.

1

Iframe Access and Appointment Type Selection

Moderate

Goal

Navigate to your booking page, locate the Acuity iframe, wait for it to load, and select a specific appointment type from the list of available services.

Preconditions

  • App running at APP_BASE_URL with the Acuity embed on /book
  • At least one active appointment type configured in Acuity
  • The embed iframe uses the standard Acuity embed code

Playwright Implementation

acuity-iframe.spec.ts

What to Assert Beyond the UI

  • The iframe actually loaded content (not a blank frame or error page)
  • The appointment type name matches your expected test configuration
  • The calendar widget appeared after selection, confirming the async transition completed

Iframe Access: Playwright vs Assrt

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

test('select appointment type from Acuity embed', async ({ page }) => {
  await page.goto('/book');

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

  const appointmentType = acuityFrame.locator(
    '.appointment-type-list .select-button'
  ).first();
  await expect(appointmentType).toBeVisible({ timeout: 15_000 });

  const typeName = acuityFrame.locator(
    '.appointment-type-list .name'
  ).first();
  await expect(typeName).toContainText('Initial Consultation');

  await appointmentType.click();

  const calendarWidget = acuityFrame.locator('.calendar-picker');
  await expect(calendarWidget).toBeVisible({ timeout: 10_000 });
});
54% fewer lines

4. Scenario: Navigating the Calendar Date and Time Picker

Acuity's date picker is a custom calendar widget, not a native HTML <input type="date">. It renders a month grid with clickable day cells. Days that have no available slots are grayed out and not clickable. Your test must find an available day (which may be in the current month or require navigating forward), click it, wait for the time slots to load asynchronously, and then select a specific time. The time slots are also custom elements, not native select dropdowns.

One of the most common failures in Acuity date tests is clicking a day cell before the calendar has finished its AJAX request for availability data. The day cells exist in the DOM before availability loads, so a naive waitForSelector will pass, but clicking on an unavailable day does nothing and the test hangs. You must wait for the availability class to be applied to at least one day cell before interacting.

2

Calendar Date and Time Slot Selection

Complex

Goal

After selecting an appointment type, pick the first available date on the calendar, select the first available time slot, and advance to the intake form.

Preconditions

  • An appointment type has been selected (Section 3 completed)
  • The Acuity account has availability in the current or next month

Playwright Implementation

acuity-date-time.spec.ts

Date/Time Picker: Playwright vs Assrt

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

test('pick first available date and time', async ({ page }) => {
  await page.goto('/book');
  const acuityFrame = page.frameLocator(
    'iframe[src*="acuityscheduling.com"]'
  );

  // Select appointment type
  await acuityFrame.locator('.select-button').first().click();

  // Wait for calendar availability data
  const availableDay = acuityFrame.locator('td.availableday').first();
  const hasDay = await availableDay.isVisible({ timeout: 5000 })
    .catch(() => false);
  if (!hasDay) {
    await acuityFrame.locator('.calendar-next').click();
    await expect(acuityFrame.locator('td.availableday').first())
      .toBeVisible({ timeout: 10_000 });
  }
  await acuityFrame.locator('td.availableday').first().click();

  // Select time slot
  const timeSlot = acuityFrame.locator('.time-slot').first();
  await expect(timeSlot).toBeVisible({ timeout: 10_000 });
  await timeSlot.click();

  // Verify intake form loaded
  await expect(acuityFrame.locator('#appointment-form'))
    .toBeVisible({ timeout: 10_000 });
});
62% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started β†’

5. Scenario: Filling Intake Forms with Custom Fields

Acuity intake forms have two layers of fields. The standard fields (name, email, phone) are always present and use predictable selectors. Custom intake fields, which the business owner configures in Acuity's admin panel, have dynamically generated IDs and can be text inputs, dropdowns, checkboxes, textareas, or file uploads. Your test must handle both layers and deal with the fact that custom field IDs are not stable across Acuity account changes.

The best strategy for custom fields is to select them by their label text rather than their ID. Acuity renders <label>elements for each custom field with the text the business owner configured. Using Playwright's getByLabel() within the frame locator gives you resilient selectors that survive Acuity admin changes as long as the label text stays the same.

3

Intake Form with Standard and Custom Fields

Moderate

Goal

Fill all required intake fields including standard fields (first name, last name, email, phone) and custom fields (dropdown selection, textarea, checkbox), then submit the form.

Playwright Implementation

acuity-intake-form.spec.ts

What to Assert Beyond the UI

  • Validation errors appear for required fields left empty
  • The confirm button is disabled until all required fields pass validation
  • Custom field values appear correctly on the confirmation page
  • The Acuity API returns the appointment with the correct intake answers

6. Scenario: Timezone Conversion and Slot Accuracy

Acuity automatically detects the user's timezone via the browser and converts all displayed time slots accordingly. A slot that shows as 2:00 PM for a user in Eastern time appears as 11:00 AM for a user in Pacific time. This is correct behavior for end users, but it creates a testing trap: if your CI server runs in UTC and your local machine runs in America/New_York, the same test selects different time slots. Worse, a slot that exists at 9:00 AM Eastern might not have a corresponding slot at 9:00 AM UTC if the business only has afternoon availability in their configured timezone.

The fix is to lock the browser timezone in your Playwright config using the timezoneId option. This ensures the Acuity embed sees the same timezone in every environment. Then verify that the displayed time matches the Acuity API response (which returns times in UTC) after conversion.

4

Timezone Verification for Booking Slots

Complex

Playwright Implementation

acuity-timezone.spec.ts

What to Assert Beyond the UI

  • The browser timezone matches your Playwright config
  • The displayed time in the embed matches the API response converted to the configured timezone
  • Slots that are unavailable in the business timezone do not appear even when the browser timezone differs

7. Scenario: Cancellation and Reschedule Flows

After a booking is created, Acuity provides cancellation and reschedule links in the confirmation email and on the confirmation page. The cancellation flow opens a separate Acuity page (not inside your embed) where the user confirms the cancellation. Rescheduling loads a modified version of the booking wizard that preserves the customer's information but lets them pick a new date and time. Both flows are critical to test because they operate outside your main embed context and use different URLs.

5

Cancel and Reschedule an Existing Booking

Complex

Goal

Create a booking, extract the cancellation and reschedule links, test both flows, and verify the appointment state changes via the API.

Playwright Implementation

acuity-cancel-reschedule.spec.ts

Cancel/Reschedule: Playwright vs Assrt

test('cancel a booking via the cancellation link', async ({ page }) => {
  // ... create booking (30+ lines) ...

  const cancelLink = acuityFrame.locator('a[href*="cancel"]');
  const cancelUrl = await cancelLink.getAttribute('href');
  expect(cancelUrl).toBeTruthy();

  await page.goto(cancelUrl!);

  const cancelButton = page.locator('button.cancel-confirm');
  await expect(cancelButton).toBeVisible({ timeout: 10_000 });
  await cancelButton.click();

  await expect(page.locator('.cancellation-confirmed'))
    .toBeVisible({ timeout: 10_000 });

  const appointments = await getRecentAppointments(testEmail);
  if (appointments.length > 0) {
    expect(appointments[0].canceled).toBe(true);
  }
});
68% fewer lines

8. Scenario: Booking Confirmation and API Verification

A common mistake in scheduling tests is to assert only on the UI confirmation page without verifying the booking actually reached Acuity's backend. The confirmation page can render before the API call completes if Acuity optimistically updates the UI. You should always double-check via the Acuity REST API that the appointment exists, has the correct fields, and is in the expected state.

Additionally, many businesses configure Acuity to send confirmation emails and trigger webhooks on booking creation. If your application relies on these webhooks (for example, to create a record in your own database), your test should also verify the downstream effect, not just the Acuity UI.

6

End-to-End Booking with API Verification

Moderate

Playwright Implementation

acuity-e2e-booking.spec.ts

9. Common Pitfalls That Break Acuity Test Suites

After building and maintaining Acuity Scheduling test suites across multiple production applications, certain failure patterns appear repeatedly. Below are the most common pitfalls, sourced from real debugging sessions and community issue reports.

Using page.locator Instead of frameLocator

The single most common mistake. Developers write page.locator('.select-button') instead of acuityFrame.locator('.select-button'). This returns zero results silently because the selector matches nothing in the host page DOM. The test hangs until timeout. Always access Acuity elements through a frameLocator reference.

Clicking Calendar Days Before Availability Loads

Acuity renders the calendar grid immediately, then fetches availability data via AJAX and applies CSS classes to mark available days. If your test clicks a day cell before the AJAX completes, the click either does nothing (the day is non-interactive) or selects a day with no slots. Wait for td.availableday to exist before interacting with the calendar.

Timezone Mismatch Between CI and Local

A test that passes locally at 3:00 PM Eastern fails in CI running in UTC because the time slot it expects (3:00 PM) does not exist when the browser reports UTC. The fix is setting timezoneId in your Playwright config so every environment sees the same slots.

Not Cleaning Up Test Bookings

Unlike mock servers, Acuity bookings are real. If you do not cancel test appointments in your afterEach hooks, they accumulate and eventually fill all available slots, causing subsequent test runs to fail because no time slots are available. This is especially insidious because it manifests as β€œno available dates” in the calendar, which looks like an Acuity outage rather than a test data leak.

Hardcoding Dates in Tests

Tests that click a specific date (like β€œApril 15”) break when that date passes or when the business has no availability on that date. Always select the first available date dynamically rather than hardcoding a calendar cell.

Acuity Testing Anti-Patterns

  • Using page.locator() instead of frameLocator() for iframe elements
  • Clicking calendar day cells before availability data loads via AJAX
  • Not setting timezoneId in Playwright config, causing CI/local slot mismatch
  • Skipping afterEach cleanup, letting test bookings fill available slots
  • Hardcoding specific calendar dates instead of selecting dynamically
  • Asserting only on the UI confirmation without verifying via the Acuity API
  • Using custom field IDs instead of label text (IDs change when admin reconfigures)
  • Running parallel tests that compete for the same time slots
Common Acuity Test Failure
Acuity Scheduling Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above requires specific knowledge of Acuity's embed structure: the iframe selector, the calendar widget class names, the intake form field names, the confirmation page selectors. When Acuity updates their embed (and they do, because Squarespace ships UI updates regularly), every selector in your test suite can break simultaneously. Assrt lets you describe the booking flow in plain English, generates the Playwright TypeScript, and automatically updates selectors when the embed DOM changes.

The intake form scenario from Section 5 demonstrates this well. In raw Playwright, you need to know that Acuity uses input[name="first_name"] for the first name field, that the confirm button has the class scheduling-confirm-button, and that the confirmation page uses .confirmation-page. In Assrt, you describe the intent and let the framework resolve everything at runtime.

scenarios/acuity-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Acuity renames a CSS class or restructures the embed DOM after a Squarespace platform update, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated selectors. Your scenario files stay untouched.

Start with the basic appointment type selection scenario. Once it is green in your CI, add the full booking flow, then timezone verification, then cancellation and reschedule. In a single afternoon you can have comprehensive Acuity Scheduling coverage that survives embed updates without manual selector maintenance.

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