Skip to main content Skip to footer

HubSpot Developer Blog

Introducing Automated Testing for UI Extensions

We're thrilled to announce the launch of comprehensive testing utilities for the HubSpot UI Extensions SDK—a feature that developers have been requesting since we first launched the platform. Today, building reliable, well-tested UI extensions becomes significantly easier with our new testing framework, complete with full TypeScript type safety.

The Challenge

If you've built UI extensions before, you know they're powerful tools for customizing the HubSpot experience. But there's been one persistent challenge: testing them in an automated way has been difficult with traditional tools.

Unlike traditional React applications that render to the browser's DOM, UI extensions run in a secure, sandboxed environment that intentionally has no DOM access. This architecture provides security and performance benefits, but it also means popular testing libraries like React Testing Library won't work out of the box. These tools are designed to test applications that render to a real DOM, and without one, they have nothing to work with.

This has left developers in a difficult position: manually testing every change or building custom testing infrastructure from scratch. Neither option is ideal, especially as your extensions grow in complexity.

The Solution

Our new testing utilities solve this problem by providing a custom test renderer that produces a test-friendly tree representation of your rendered UI extension output. Instead of relying on a DOM, we create a lightweight tree structure that mirrors your rendered component hierarchy, making it easy to query, inspect, and interact with your rendered components. All functions that would typically call HubSpot services are automatically mocked for fast and reproducible tests.

The result is a testing experience that feels familiar and intuitive, with powerful querying capabilities, comprehensive mocking support, and utilities specifically designed for UI extensions. If you're using TypeScript (which we highly recommend), you'll benefit from complete type safety throughout your tests—from component props to mock configurations, the TypeScript compiler catches errors before you even run your tests, and your IDE provides intelligent autocomplete every step of the way.

Getting Started

The testing utilities are available now in the @hubspot/ui-extensions package. These utilities are compatible with any test runner—whether you prefer Jest, Mocha, or another testing framework; however, we recommend Vitest for its speed, modern features, and excellent developer experience.

Installation

First, install the UI Extensions SDK:

npm install @hubspot/ui-extensions@latest

Then, install Vitest as a dev dependency:

npm install --save-dev vitest

Basic Example

The createRenderer() function is your entry point. It requires an extension point location (like 'crm.record.tab' or 'settings') and automatically configures appropriate mocks for that location's APIs.

Here's a simple example to get you started:

import { createRenderer } from '@hubspot/ui-extensions/testing'; import { Button, Text } from '@hubspot/ui-extensions'; function MyExtension() { return ( <> <Text>Hello!</Text> <Button variant="primary">Click me!</Button> </> ); } describe('my extension', () => { it('should render a button', () => { // Create a renderer for your extension point const { render, find } = createRenderer('crm.record.tab'); // Render your component render(<MyExtension />); // Query and make assertions const button = find(Button); expect(button.props.variant).toEqual('primary'); expect(button.text).toEqual('Click me!'); }); });

Key Features

1. Powerful Querying

Find components using flexible query methods:

it('should render the correct buttons', () => { const { render, find, findAll, findByTestId } = createRenderer('crm.record.tab'); render( <ButtonRow> <Button variant="secondary" testId="cancel-button">Cancel</Button> <Button variant="primary" testId="submit-button">Submit</Button> </ButtonRow> ); // Find any button const anyButton = find(Button); // Find buttons with specific props const primaryButton = find(Button, { variant: 'primary' }); // Find using a custom predicate const submitButton = find( Button, (node) => node.text === 'Submit' ); // Find by testId (recommended for stable tests) const cancelButton = findByTestId(Button, 'cancel-button'); expect(cancelButton.text).toEqual('Cancel'); // Find all buttons const allButtons = findAll(Button); expect(allButtons).toHaveLength(2); });

2. Event Triggering

Test user interactions by triggering event handlers:

function Counter() { const [count, setCount] = useState(0); return ( <Button onClick={() => setCount(count + 1)}> Clicked {count} times </Button> ); } it('should increment count when button is clicked', () => { const { render, find } = createRenderer('crm.record.tab'); render(<Counter />); expect(find(Button).text).toEqual('Clicked 0 times'); // Trigger the click event find(Button).trigger('onClick'); expect(find(Button).text).toEqual('Clicked 1 times'); });

3. Async Testing Support

Wait for asynchronous updates with the built-in waitFor() utility:

function AsyncComponent() { const [data, setData] = useState(null); useEffect(() => { setTimeout(() => { setData('Loaded!'); }, 100); }, []); return <Text>{data || 'Loading...'}</Text>; } it('should display loaded data after async operation completes', async () => { const { render, find, waitFor } = createRenderer('crm.record.tab'); render(<AsyncComponent />); expect(find(Text).text).toEqual('Loading...'); await waitFor(() => { expect(find(Text).text).toEqual('Loaded!'); }); });

4. Comprehensive Mocking

Test components that use HubSpot APIs without making actual service calls. The testing SDK automatically provides mocks for:

•  React hooks (useCrmProperties(), useAssociations())
•  Extension Point API (context, actions, runServerlessFunction())

Here's how easy it is to use mocks for HubSpot-provided React hooks. In your UI components, you continue to import and use the hooks as you normally would:

UserGreeting.tsx:

import { useCrmProperties } from "@hubspot/ui-extensions/crm"; function UserGreeting() { const { properties, isLoading, error } = useCrmProperties([ 'firstname', 'lastname' ]); if (error) { return <Text>Error loading properties</Text>; } return <Text>Hello, {properties.firstname}!</Text>; }

With the provided React hook mocks, you can now easily test your component—including all edge cases. Here's how you might test a failure scenario:

UserGreeting.test.tsx:

it('should render an error message when CRM properties fail to load', () => { const { render, mocks, find } = createRenderer('crm.record.tab'); // Mock an error state mocks.useCrmProperties.nextResult({ properties: {}, error: new Error('Network error'), isLoading: false, }); render(<UserGreeting />); expect(find(Text).text).toEqual('Error loading properties'); });

5. Debugging Tools

When tests don't behave as expected, use the built-in debugging utilities:

it('should render correctly', () => { const { render, debugLog, find } = createRenderer('crm.record.tab'); render( <Flex direction="column"> <Button variant="primary">Submit</Button> <Button variant="secondary">Cancel</Button> </Flex> ); // Log the entire tree debugLog('RENDERED OUTPUT'); /* Console output: ============ RENDERED OUTPUT <Flex direction="column"> <Button variant="primary"> "Submit" </Button> <Button variant="secondary"> "Cancel" </Button> </Flex> ============ */ // Log a specific element find(Button, { variant: 'primary' }).debugLog('PRIMARY BUTTON'); /* Console output: ============== PRIMARY BUTTON <Button variant="primary"> "Submit" </Button> ============== */ });

Complete Documentation

We've created comprehensive documentation to help you get the most out of the testing utilities:

 

What This Means for You

With these new testing utilities, you can:

  • Build with confidence - Catch bugs before they reach production
  • Refactor safely - Make changes knowing your tests will catch regressions
  • Document behavior - Tests serve as executable documentation for how your extensions work
  • Move faster - Automated tests are faster than manual testing and run consistently every time
  • Improve quality - Well-tested extensions are more reliable and maintainable

 

Get Started Today

The testing utilities are available now in the latest version of @hubspot/ui-extensions. Update your package and start writing tests:

npm install @hubspot/ui-extensions@latest

We're excited to see what you build with these new testing capabilities. We welcome your feedback and suggestions—let us know what you think in the developer forums or reach out to our developer support team.

Happy testing! 🧪

 


Resources


Patrick Steele-Idem

Patrick Steele-Idem is a Staff Software Engineer at HubSpot dedicated to making the platform more extensible for developers. With a passion for frontend development, he enjoys building platforms that directly empower and delight other engineers. When he's not developing software, Patrick welcomes the opportunity to be outdoors, often found hiking and climbing mountains.