Testing Symbol-Keyed Internal State
The previous page exported the tracking symbol under an /** @internal */ tag specifically so it stays inspectable — mainly for the mixin's own test suite. This page covers how to actually use it from a test.
The Problem: TypeScript Doesn't Know About the Symbol Key
Once a property is keyed by a Symbol instead of a string, you can't just write instance._managedForms anymore — there is no such property. You also can't write instance[managedForms] and expect it to type-check cleanly, because the mixin's public type (WithFormMixin, in the withForm case) only declares the methods it's meant to expose (createForm, dispose) — not the internal tracking array, which was never part of that public interface in the first place.
The fix is a small test-local type that describes just the internal shape you need to assert against:
import { managedForms, type GeaForm } from '../src/index'
interface ManageProperty {
[managedForms]: GeaForm[]
}
This type exists only in the test file. It isn't exported from the library, and it isn't meant to be — it's a narrow, test-specific window into internal state, not a public contract.
Applying It to a Test Instance
Intersect the component instance's inferred type with ManageProperty to get a type-safe handle on the internal array, without touching the production type definitions at all:
class MyComponent extends withForm(Component) {}
const instance = new MyComponent()
type TestInstance = typeof instance & ManageProperty
const testInstance = instance as TestInstance
const form = testInstance.createForm<LoginValues>(makeLoginSchema())
expect(testInstance[managedForms]).toHaveLength(1)
expect(testInstance[managedForms][0]).toBe(form)
testInstance[managedForms] is now fully typed as GeaForm[], with autocomplete and type-checking, while nothing about the mixin's actual public surface (what a real consumer would see) had to change to make this possible.
Verifying Disposal
The main reason to reach into this internal state in a test at all is to confirm cleanup actually happens — the exact thing a black-box test (only calling public methods) can't directly observe:
it('destroys all managed forms and clears the registry on dispose', () => {
class MyComponent extends withForm(Component) {}
const instance = new MyComponent()
const testInstance = instance as typeof instance & ManageProperty
const form = testInstance.createForm<LoginValues>(makeLoginSchema())
const destroySpy = vi.spyOn(form, 'destroy')
testInstance.dispose()
expect(destroySpy).toHaveBeenCalledTimes(1)
expect(testInstance[managedForms]).toHaveLength(0)
})
Without access to the symbol, this test could only check that dispose() didn't throw — it couldn't confirm that every tracked GeaForm actually got torn down, which is the entire point of the mixin existing.
Why Not Just Make It Public?
It might seem simpler to skip all of this and just expose managedForms as a normal, documented public property. The reason not to: doing so invites consumers to read or mutate it directly, which locks the mixin into that exact internal representation forever — any future refactor (switching from an array to a Map, say) becomes a breaking change instead of an implementation detail. Keeping it @internal, symbol-keyed, and only reachable through a deliberate test-local type preserves the freedom to change the internals later without it counting as a breaking change for real consumers.