Testing
Entangle UI components are plain React components, so any React testing stack works. Two things differ from testing hand-written components, and both have a one-time setup:
- The library ships side-effect CSS imports (each component pulls in its own stylesheet), so your test runner must transform the package instead of externalizing it.
- Components read design tokens from
--etui-*CSS custom properties on:root, and several editor components use browser observers (ResizeObserver,IntersectionObserver,matchMedia) that jsdom does not implement.
This guide uses Vitest + @testing-library/react (the same stack the library itself uses), but the concepts map directly to Jest.
Vitest configuration
Section titled “Vitest configuration”import { defineConfig } from 'vitest/config';import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', css: true, setupFiles: ['./test/setup.ts'], // Entangle UI is ESM-only and imports its CSS as a side effect // (`import './Button.css'`). Vitest externalizes `node_modules` by default, // and Node cannot import a `.css` file as a module — so the package must be // inlined to run through Vitest's transform pipeline. server: { deps: { inline: ['entangle-ui'], }, }, },});environment: 'jsdom'gives you a DOM.happy-domalso works.css: truelets Vitest process the stylesheets the components import, so the injected:rootrule is visible togetComputedStyle(see below).server.deps.inline: ['entangle-ui']is the key line. Without it you will see errors such asUnknown file extension ".css"orCannot find module './Button.css'the moment a test imports a component.
Loading the theme so CSS variables resolve
Section titled “Loading the theme so CSS variables resolve”Components reference tokens like var(--etui-color-bg-primary). Those variables only exist once the dark theme stylesheet is registered on :root. Import the canonical stylesheet once in your setup file:
import '@testing-library/jest-dom/vitest';
// Registers every --etui-* token on :root (the default dark theme) so// components resolve their CSS variables in jsdom. This is the same single// import you use in your app entry.import 'entangle-ui/styles.css';With that in place, assertions against computed styles work:
import { render, screen } from '@testing-library/react';import { Button } from 'entangle-ui';
it('paints the themed surface', () => { render(<Button>Save</Button>); const styles = getComputedStyle(screen.getByRole('button')); // --etui-* values are now resolved from the :root rule expect(styles.getPropertyValue('--etui-color-bg-primary').trim()).toBe( '#1a1a1a' );});If you skip the import, components still render and remain queryable by role/text — only color/spacing assertions that depend on the tokens will come back empty.
Stubbing browser observers
Section titled “Stubbing browser observers”jsdom implements neither ResizeObserver, IntersectionObserver, nor a real matchMedia. Several components use them:
ResizeObserver—SplitPane,SegmentedControl, and the editor surfaces (NodeGraph,Timeline,Minimap,ViewportGizmo,CurveEditor,CartesianPicker,AssetBrowser). Rendering any of these without a stub throwsResizeObserver is not defined.IntersectionObserver— anything using theuseIntersectionObserverhook.matchMedia— the reduced-motion checks inTooltip,Dialog,Drawer, and viewport components readwindow.matchMedia('(prefers-reduced-motion: reduce)').
Add minimal stubs to the same setup file:
// test/setup.ts (continued)import { vi } from 'vitest';
if (typeof globalThis.ResizeObserver === 'undefined') { globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} };}
if (typeof globalThis.IntersectionObserver === 'undefined') { globalThis.IntersectionObserver = class { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } root = null; rootMargin = ''; thresholds = []; } as unknown as typeof IntersectionObserver;}
Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, // reduced motion off by default media: query, onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), })),});To assert reduced-motion behavior, make matchMedia(...).matches return true for the query under test.
A renderWithTheme helper
Section titled “A renderWithTheme helper”For tokens to resolve you only need the styles.css import above. Use ThemeProvider when a test needs its runtime behaviors — keyboard context or the opt-in globalScrollbars. A small wrapper keeps tests tidy:
import { render, type RenderOptions } from '@testing-library/react';import { ThemeProvider } from 'entangle-ui';import type { ReactElement, ReactNode } from 'react';
const Wrapper = ({ children }: { children: ReactNode }) => ( <ThemeProvider>{children}</ThemeProvider>);
export function renderWithTheme( ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) { return render(ui, { wrapper: Wrapper, ...options });}import { renderWithTheme } from '../test/renderWithTheme';import { screen } from '@testing-library/react';import { Slider } from 'entangle-ui';
it('renders a slider', () => { renderWithTheme(<Slider defaultValue={50} aria-label="Volume" />); expect(screen.getByRole('slider')).toBeInTheDocument();});Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
| ---------------------------------------------------------------- | --------------------------- | --------------------------------------------------- |
| Unknown file extension ".css" / Cannot find module './*.css' | Package is externalized | Add server.deps.inline: ['entangle-ui'] |
| getPropertyValue('--etui-…') returns '' | Theme stylesheet not loaded | import 'entangle-ui/styles.css' in the setup file |
| ResizeObserver is not defined | jsdom has no observers | Add the ResizeObserver stub above |
| window.matchMedia is not a function | jsdom has no matchMedia | Add the matchMedia stub above |
See also
Section titled “See also”- Installation — the same
entangle-ui/styles.cssimport used in your app. - Theming —
ThemeProvider, custom themes, and the--etui-*token reference.