Skip to content
Entangle UI v0.13.0

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:

  1. 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.
  2. 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.config.ts
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-dom also works.
  • css: true lets Vitest process the stylesheets the components import, so the injected :root rule is visible to getComputedStyle (see below).
  • server.deps.inline: ['entangle-ui'] is the key line. Without it you will see errors such as Unknown file extension ".css" or Cannot 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:

test/setup.ts
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.

jsdom implements neither ResizeObserver, IntersectionObserver, nor a real matchMedia. Several components use them:

  • ResizeObserverSplitPane, SegmentedControl, and the editor surfaces (NodeGraph, Timeline, Minimap, ViewportGizmo, CurveEditor, CartesianPicker, AssetBrowser). Rendering any of these without a stub throws ResizeObserver is not defined.
  • IntersectionObserver — anything using the useIntersectionObserver hook.
  • matchMedia — the reduced-motion checks in Tooltip, Dialog, Drawer, and viewport components read window.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.

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:

test/renderWithTheme.tsx
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();
});

| 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 |

  • Installation — the same entangle-ui/styles.css import used in your app.
  • ThemingThemeProvider, custom themes, and the --etui-* token reference.