Rich, web-friendly editor UI framework for SolidJS + TypeScript
solid-stage docs testing.md
4.7 kB · 112 lines
Markdown
at main

Testing a consumer #

This is about testing your app when it imports @dolphinspired/solid-stage — not about the library's own suite. If your Vitest tests render a component that imports the library (directly or transitively), you'll hit this on the first run:

TypeError: Unknown file extension ".jsx" for …/@kobalte/core/dist/text-field/index.jsx

Why it happens #

The library is shipped as pre-compiled JS — it isn't the source of the .jsx. The error comes from its dependency @kobalte/core (the headless field primitives) and three packages Kobalte pulls in — @corvu/utils, solid-presence, solid-prevent-scroll — which all ship .jsx in their dist/. Node can't parse .jsx, and Vitest externalizes node_modules by default (loads them through Node, untransformed), so the import blows up.

The fix is to tell Vitest to inline those packages so vite-plugin-solid transforms their .jsx like your own source.

The config #

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import solid from 'vite-plugin-solid';

export default defineConfig({
  plugins: [solid()],
  test: {
    environment: 'happy-dom',
    server: {
      deps: {
        // The Kobalte ecosystem ships .jsx in dist — inline so vite-plugin-solid
        // transforms it. These four are the complete set (verified: @kobalte/utils
        // ships plain JS and does NOT need inlining).
        inline: [
          /@kobalte\/core/,
          /@corvu\/utils/,
          /solid-presence/,
          /solid-prevent-scroll/,
        ],
      },
    },
  },
  // Without this, the inlined deps pull a second copy of Solid and reactivity
  // silently breaks across the boundary ("multiple instances of solid-js").
  resolve: { dedupe: ['solid-js'] },
});

That's the whole fix. Each piece, and why:

Piece Why
inline: [/@kobalte\/core/, /@corvu\/utils/, /solid-presence/, /solid-prevent-scroll/] The only .jsx-shipping packages in the tree. Inlining routes them through vite-plugin-solid.
resolve.dedupe: ['solid-js'] Forces one Solid instance. Solid's reactivity is instance-global; two copies means signals from one don't track in the other.
environment: 'happy-dom' (or jsdom) The library's components reference browser globals (document, getBoundingClientRect, guarded ResizeObserver/IntersectionObserver). A test that imports them needs a DOM.

You do not need to inline @dolphinspired/solid-stage itself. It ships pre-compiled JS, so Node loads it fine and the deduped solid-js reaches it. Inlining it too is harmless (one rule for the whole library surface) but redundant — the .jsx only ever comes from the Kobalte-ecosystem deps above.

Developing against a local link:ed checkout of the library (not the published package)? The same solid-js dedupe applies to your dev server and build, plus a couple more link-only steps — see Developing against a local checkout.

Per-file environment instead of global #

If you'd rather not set environment globally, drop a docblock at the top of each test file that touches the library (this is what the library's own suite does):

// @vitest-environment happy-dom

A minimal smoke test #

// src/MyToolbar.test.tsx
// @vitest-environment happy-dom
import { render } from '@solidjs/testing-library';
import { expect, it } from 'vitest';
import { MyToolbar } from './MyToolbar'; // imports TextField from the library

it('renders', () => {
  const { container } = render(() => <MyToolbar />);
  expect(container.querySelector('.stage-field-control')).not.toBeNull();
});

If this runs green, your config is correct and the rest of your suite will import the library without the .jsx error.

Driving Kobalte primitives in a test is fiddly — the event that commits a value differs per primitive (text/number fields fire on input; switch / checkbox / slider fire on change; open a Select/Color overlay with pointerdown + pointerup, not .click()). The library's own *.test.tsx files next to each control are the reference for the exact gesture.

Driving a section drag in a test works with real PointerEvents dispatched on the section's .stage-section-drag-handle: pointerdown, then pointermove past 4px, then pointerup at the drop point. happy-dom has no layout, so stub getBoundingClientRect on the docks and cards involved. sectionDrag.test.tsx in the library is the reference. A synthetic drag proves the logic, not the feel: judge smoothness by eye with a real pointer.