Form Testing Guide

How to Test HubSpot Form Submission with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing HubSpot embedded forms with Playwright. Cookie consent banners, tracked submissions with HubSpot analytics, async success messages, progressive profiling, dependent fields, and the pitfalls that break real HubSpot form test suites.

200K+

Over 200,000 companies in more than 120 countries use HubSpot, and forms are the primary lead capture mechanism across its Marketing Hub, generating millions of submissions daily.

HubSpot 2025 Annual Report

0Form scenarios covered
0sAvg async wait for success
0%Fewer lines with Assrt
0Hidden tracking fields verified

HubSpot Embedded Form Submission Flow

BrowserYour PageHubSpot JSHubSpot APICRMLoad page with embedded formLoad hs-forms embed scriptRender form into DOMFill fields and submitPOST /submissions/v3/integration/submitCreate/update contact in CRM200 OK with redirect/messageDisplay success message or redirect

1. Why Testing HubSpot Forms Is Harder Than It Looks

HubSpot embedded forms look simple on the surface. You drop a JavaScript snippet onto your page, the form renders, users fill it out, and submissions land in your HubSpot CRM. But the moment you try to automate this flow with Playwright, five structural challenges surface that make reliable testing significantly more complex than testing a standard HTML form.

First, the form is injected asynchronously by HubSpot's hbspt.forms.create() JavaScript. The form does not exist in the initial HTML payload. Playwright must wait for the HubSpot script to load, initialize, and render the form into its target container before any locator can find the input fields. A naive page.goto() followed immediately by a page.fill() will fail because the inputs do not yet exist in the DOM.

Second, cookie consent banners interact with HubSpot tracking in ways that affect form behavior. HubSpot's tracking code respects the cookie consent banner settings. If the user has not accepted analytics cookies, HubSpot may skip setting the hutk (HubSpot tracking cookie), which means form submissions will not be attributed to previous page visits. Your test needs to handle the consent banner before or during form interaction, and you need separate test paths for consented and non-consented submissions.

Third, the success state after submission is asynchronous. HubSpot forms POST to the HubSpot API (not to your backend), and the success message, redirect, or inline “thank you” content appears only after the API responds. Network latency, HubSpot API throttling, or a slow CRM workflow can delay this response by several seconds. Hard-coded waits are brittle; you need to poll for the success indicator with a reasonable timeout.

Fourth, progressive profiling changes the form between visits. HubSpot can be configured to show different fields to returning visitors who already submitted certain data. A contact who previously provided their company name will see a different set of fields on their second visit, potentially including job title, phone number, or budget range. Your test must account for the fact that the form shape is not static; it depends on CRM state.

Fifth, dependent fields (also called conditional logic) show or hide inputs based on other field values. Selecting “Enterprise” from a company size dropdown might reveal a “Number of employees” field, while selecting “Startup” hides it. These transitions are handled by HubSpot's client-side JavaScript, and the newly revealed fields need time to render before Playwright can interact with them.

HubSpot Form Lifecycle on Page Load

🌐

Page Loads

Initial HTML rendered

⚙️

Script Fetch

hs-forms.js downloaded

⚙️

Form Init

hbspt.forms.create() called

🌐

DOM Injection

Form HTML injected into container

🔒

Cookie Check

Read hutk, consent state

Ready

Fields interactive, tracking active

Progressive Profiling Decision Flow

🌐

Visitor Arrives

Page loads form

🔒

Cookie Lookup

hutk identifies contact

⚙️

CRM Query

Fetch known properties

Field Filter

Hide already-collected fields

🌐

New Fields

Show profiling questions

A good HubSpot form test suite addresses all five of these challenges. The sections below walk through each scenario with runnable Playwright TypeScript you can paste directly into your project.

2. Setting Up a Reliable Test Environment

Before writing any test scenarios, you need a HubSpot sandbox environment and a Playwright configuration tuned for the async nature of embedded forms. HubSpot offers a free developer test account that supports all form features including progressive profiling, dependent fields, and analytics tracking. Never run automated tests against your production HubSpot portal; the API rate limits are strict and you will pollute your real CRM with test contacts.

HubSpot Test Environment Setup Checklist

  • Create a HubSpot developer test account (free tier supports all form features)
  • Create a test form with standard fields: email, first name, last name, company
  • Enable progressive profiling on the form with secondary fields: job title, phone
  • Add a dependent field: company size dropdown that reveals employee count
  • Configure the cookie consent banner in Settings > Privacy & Consent
  • Note your portal ID (hub_id) and form GUID from the form editor URL
  • Generate a private app token with contacts and forms scopes for API cleanup
  • Add the HubSpot embed script to your test page template

Environment Variables

.env.test

Contact Cleanup via HubSpot API

Every test run should start with a clean slate. Use the HubSpot private app API to search for and delete test contacts before each suite. This prevents progressive profiling and deduplication logic from interfering with your expected form state.

test/helpers/hubspot-cleanup.ts

Playwright Configuration for HubSpot Forms

HubSpot forms load asynchronously, so your Playwright config needs generous timeouts for the initial form render. The form script fetches from js.hsforms.net and can take one to three seconds depending on network conditions. Set action timeouts high enough to survive slow CDN responses in CI.

playwright.config.ts
Installing Dependencies

3. Scenario: Basic Form Submission Happy Path

The first scenario every HubSpot form integration needs is the basic submission that succeeds without any complications. This is your smoke test. If this breaks, no leads are coming in and you want to know immediately. The flow is: your page loads with the HubSpot embed script, the form renders asynchronously into its container, the user fills in the required fields, clicks submit, the form POSTs to HubSpot's API, and a success message appears.

The critical subtlety is waiting for the form to exist in the DOM before interacting with it. HubSpot forms are rendered by JavaScript after the page loads. You need to wait for the form container to contain actual input elements, not just the empty target div.

1

Basic Form Submission Happy Path

Straightforward

Goal

Navigate to the page containing an embedded HubSpot form, wait for the form to render, fill in required fields, submit, and confirm the success message appears.

Preconditions

  • App running at APP_BASE_URL with the HubSpot form embed
  • No existing CRM contact for the test email address
  • Cookie consent banner either dismissed or not enabled

Playwright Implementation

hubspot-form.spec.ts

What to Assert Beyond the UI

Submission Verification Checklist

  • Success message or redirect appears within 10 seconds
  • The form container is replaced with the thank-you content
  • No JavaScript errors in the browser console during submission
  • The HubSpot API call returned HTTP 200 (verify via network interception)

Basic Submission: Playwright vs Assrt

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

test('basic HubSpot form submission', async ({ page }) => {
  const testEmail = `lead+${Date.now()}@yourapp-test.mailosaur.net`;
  await page.goto('/contact');

  const formContainer = page.locator('.hs-form');
  await formContainer.waitFor({ state: 'visible', timeout: 15_000 });

  const emailInput = page.locator('.hs-form input[name="email"]');
  await emailInput.waitFor({ state: 'visible', timeout: 10_000 });

  await emailInput.fill(testEmail);
  await page.locator('.hs-form input[name="firstname"]').fill('Test');
  await page.locator('.hs-form input[name="lastname"]').fill('User');
  await page.locator('.hs-form input[name="company"]').fill('Acme Corp');

  await page.locator('.hs-form input[type="submit"]').click();

  const successMessage = page.locator('.submitted-message');
  await expect(successMessage).toBeVisible({ timeout: 10_000 });
  await expect(successMessage).toContainText(/thank you/i);
});
46% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Verifying HubSpot Analytics Tracking

Form submissions are only half the story. HubSpot's tracking code (hs-script-loader.js) sends page view events, tracks visitor identity, and associates form submissions with the visitor's browsing session. If the tracking code is missing or misconfigured, your form still works but the CRM contact record will lack page view history, source attribution, and campaign data. For marketing teams, this data loss is as critical as a broken form.

Testing analytics tracking requires intercepting network requests. Playwright's page.route() and page.waitForRequest() APIs let you observe the HubSpot tracking calls without modifying any application code. You can verify that the tracking script loaded, that page view events fired, and that the form submission payload includes all expected hidden fields.

3

Verifying HubSpot Analytics Tracking

Complex

Playwright Implementation

hubspot-analytics.spec.ts

Hidden Fields to Verify in Every Submission

HubSpot forms automatically include several hidden context fields in the submission payload. These fields are not visible in the DOM but are critical for CRM attribution. Your test should verify their presence.

hubspot-hidden-fields.ts

6. Scenario: Progressive Profiling (Returning Visitors)

Progressive profiling is one of HubSpot's most powerful form features, and one of the most challenging to test. When enabled, HubSpot tracks which fields a contact has already submitted and replaces those fields with new questions on subsequent visits. A first-time visitor sees email, name, and company fields. When that same contact returns, those fields are hidden and replaced with job title, phone number, or budget range.

The identification mechanism is the hutktracking cookie. When the form loads, HubSpot's JavaScript reads the cookie, queries the CRM for known properties, and dynamically hides fields that already have values. This means your test must simulate a returning visitor by either preserving the hutk cookie from a previous submission or using the HubSpot API to pre-populate contact properties before the second form load.

4

Progressive Profiling: Returning Visitor

Complex

Playwright Implementation

hubspot-progressive.spec.ts

Progressive Profiling: Playwright vs Assrt

test('progressive profiling: second visit', async ({ page, context }) => {
  const testEmail = `profile+${Date.now()}@test.mailosaur.net`;
  await deleteTestContact(testEmail);

  // First visit: submit basic fields
  await page.goto('/contact');
  await page.locator('#hs-eu-confirmation-button').click();
  const emailInput = page.locator('.hs-form input[name="email"]');
  await emailInput.waitFor({ state: 'visible', timeout: 15_000 });
  await emailInput.fill(testEmail);
  await page.locator('.hs-form input[name="firstname"]').fill('Profile');
  await page.locator('.hs-form input[name="lastname"]').fill('Test');
  await page.locator('.hs-form input[name="company"]').fill('Inc');
  await page.locator('.hs-form input[type="submit"]').click();
  await expect(page.locator('.submitted-message')).toBeVisible();

  // Second visit: verify different fields
  await page.waitForTimeout(5_000);
  await page.goto('/contact');
  await expect(page.locator('input[name="firstname"]')).toBeHidden();
  const jobtitle = page.locator('input[name="jobtitle"]');
  await jobtitle.waitFor({ state: 'visible', timeout: 10_000 });
  await jobtitle.fill('VP of Engineering');
  await page.locator('input[name="phone"]').fill('+1-555-0199');
  await page.locator('.hs-form input[type="submit"]').click();
  await expect(page.locator('.submitted-message')).toBeVisible();
});
54% fewer lines

7. Scenario: Dependent and Conditional Fields

Dependent fields (also called conditional logic in HubSpot) show or hide form inputs based on the values of other fields. A common example: a “Company Size” dropdown with options like “1-10”, “11-50”, “51-200”, and “201+” that conditionally reveals a “Number of Developers” text input when the user selects “201+”. The reveal animation is handled by HubSpot's client-side JavaScript, and the newly visible field needs a moment to become interactive.

Testing dependent fields requires a two-phase interaction: first select the trigger value, then wait for the dependent field to render before filling it. If you try to fill the dependent field immediately after selecting the trigger, Playwright may fail because the field does not yet exist in the DOM or is still animating in.

5

Dependent Fields: Company Size Reveals Details

Moderate

Playwright Implementation

hubspot-dependent.spec.ts

8. Scenario: Client-Side Validation and Error States

HubSpot forms include built-in client-side validation for required fields, email format, phone number format, and custom validation rules you configure in the form editor. When a user submits a form with invalid data, HubSpot highlights the offending fields with error messages and prevents the submission. Testing these error states ensures your form configuration is correct and that the error UX works as expected.

The validation messages are rendered by HubSpot's JavaScript, not by the browser's native validation. This means they appear as custom DOM elements with HubSpot-specific class names. You need to target these HubSpot error elements, not the standard HTML5 :invalid pseudo-class.

6

Client-Side Validation Errors

Straightforward

Playwright Implementation

hubspot-validation.spec.ts

9. Common Pitfalls That Break HubSpot Form Test Suites

Form Not Rendered: Racing the Async Embed

The most common failure in HubSpot form tests is interacting with fields before the form has rendered. The HubSpot embed script (hs-forms.js) loads asynchronously from js.hsforms.net, and the form is injected into the DOM only after the script initializes. In CI environments with slow network access, this can take five seconds or more. Always wait for the form container to become visible and for at least one input field to exist before filling. Never rely on page.goto() alone to guarantee form readiness. A common pattern from GitHub issues (hsforms/embed-js#247) is tests failing only in CI because the CDN response time is two to four times slower than local development.

The hutk Cookie and Third-Party Cookie Blocking

Modern browsers increasingly block third-party cookies by default. The hutkcookie is set by HubSpot's tracking script on your domain (first-party), but if your site loads the HubSpot script from a different origin and the browser blocks cross-site tracking, the cookie may not be set. In Playwright, the default Chromium context allows third-party cookies, so your tests may pass locally but the behavior may differ for real users. Test both with and without the hutk cookie to cover both scenarios.

HubSpot API Rate Limits on Form Submissions

The HubSpot form submissions API enforces a rate limit of 100 submissions per 10 seconds per form in production portals. In sandbox environments, this limit is lower. If you run a parallel Playwright suite with many workers all submitting the same form, you will see HTTP 429 responses. The form will appear to submit successfully on the client side (HubSpot queues the submission), but the CRM contact may not be created immediately. Limit your parallel workers to two or three for form submission tests.

Stale Progressive Profiling State

Progressive profiling depends on the CRM knowing which properties the contact already has. If your test cleanup deletes the contact but the hutk cookie still exists in the browser, HubSpot may show unpredictable field combinations. The hutk cookie is tied to an anonymous visitor record that persists even after the contact is deleted. For clean progressive profiling tests, clear both the CRM contact and the browser cookies between test runs.

Form GUID Changes After Cloning

When you clone a HubSpot form in the form editor, the clone gets a new GUID. If your embed code references the form by GUID (which it always does), cloning a form for testing purposes means updating every page that embeds it. A safer approach is to use a dedicated test form GUID stored in an environment variable, never hardcode GUIDs in your page templates or test files.

HubSpot Form Test Suite Run
Common Error: Form Not Rendered in Time

10. Writing These Scenarios in Plain English with Assrt

Every scenario above requires intimate knowledge of HubSpot's CSS class names, DOM structure, cookie mechanics, and async timing. The selectors like .hs-form input[name="email"], #hs-eu-cookie-confirmation, and .submitted-messageare specific to HubSpot's current embed implementation. When HubSpot ships an update that renames a class, restructures the form DOM, or changes the consent banner markup, every test in your suite breaks simultaneously.

Assrt lets you describe the scenario intent in plain English and handles the selector resolution at runtime. When HubSpot changes the form structure, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files stay untouched.

The progressive profiling scenario from Section 6 demonstrates the power of this approach. In raw Playwright, you need to know the exact input name attributes, the cookie consent banner IDs, and the timing between first and second visits. In Assrt, you describe the intent and let the framework resolve the details.

scenarios/hubspot-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 HubSpot renames a CSS class from .hs-form to .hs-form-v2 or restructures the consent banner, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files stay untouched.

Start with the basic submission happy path. Once it is green in your CI, add the cookie consent scenario, then progressive profiling, then dependent fields, then validation errors. In a single afternoon you can have complete HubSpot form coverage that most marketing teams never manage to achieve 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