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.
“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
HubSpot Embedded Form Submission Flow
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
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.
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.
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.
Basic Form Submission Happy Path
StraightforwardGoal
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_URLwith the HubSpot form embed - No existing CRM contact for the test email address
- Cookie consent banner either dismissed or not enabled
Playwright Implementation
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);
});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.
Verifying HubSpot Analytics Tracking
ComplexPlaywright Implementation
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.
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.
Progressive Profiling: Returning Visitor
ComplexPlaywright Implementation
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();
});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.
Dependent Fields: Company Size Reveals Details
ModeratePlaywright Implementation
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.
Client-Side Validation Errors
StraightforwardPlaywright Implementation
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.
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.
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.