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.
β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
Acuity Scheduling Embed Booking Flow
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
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.
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.
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.
Iframe Access and Appointment Type Selection
ModerateGoal
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_URLwith 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
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 });
});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.
Calendar Date and Time Slot Selection
ComplexGoal
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
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 });
});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.
Intake Form with Standard and Custom Fields
ModerateGoal
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
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.
Timezone Verification for Booking Slots
ComplexPlaywright Implementation
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.
Cancel and Reschedule an Existing Booking
ComplexGoal
Create a booking, extract the cancellation and reschedule links, test both flows, and verify the appointment state changes via the API.
Playwright Implementation
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);
}
});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.
End-to-End Booking with API Verification
ModeratePlaywright Implementation
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
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.
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
How to Test Cal.com Booking
A practical, scenario-by-scenario guide to testing Cal.com booking flows with Playwright....
How to Test Calendly Booking Flows
A practical, scenario-by-scenario guide to testing Calendly booking flows with...
How to Fix Flaky Tests
Root causes and proven fixes for unreliable tests.
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.