← All posts
Concepts

How framework plugins work under the hood

Some UIs are hard to drive from the outside. A component library that renders a dropdown as a <div> soup and ignores select_option. A data grid that keeps 20 rows in the DOM out of 8,000 on the server. A ribbon of icon-only buttons with no accessible names. Browser-level automation can technically reach all of it, but it burns time rediscovering the same quirks a component author already knows by heart.

That's the gap a framework plugin closes. Everything TestVibe does by default is framework-agnostic — it drives whatever the browser renders. A plugin adds framework-specific knowledge on top: it teaches the generator how this widget wants to be driven, and gives the generated tests trusted shortcuts for the parts that pixels can't reach. The shipped exemplar is the Wisej.NET plugin (built by Ice Tea Group, the makers of Wisej.NET — and TestVibe itself runs on Wisej, so it's dogfooded daily). But the plugin surface is general, and it's worth understanding because the first question anyone with an unusual stack asks is: will this work on my framework?

The floor: what every framework gets for free

Before any plugin, the baseline is already high, because generation targets the browser, not an SDK. There's nothing to install in your app, no instrumentation, no version bump. A black-box test drives the page the way a user does, and the framework — and the framework version — behind that page is invisible to it.

That baseline handles a lot of what people assume needs special support:

That's the floor. It's enough for most apps. A plugin is what you reach for when your framework does something the floor can't see — and it does it in three distinct layers.

Layer 1: an init script that repairs testability

The first layer runs inside the page at test time. It's a script the plugin injects before the test interacts with anything, and its job is to make the app more testable without touching the app's source.

Concretely, the Wisej init script does two things. It repairs missing ARIA metadata — a lot of component frameworks emit controls that render fine but carry no accessible role or name, which is exactly what makes role-based locators fail. And it stamps stable test ids onto framework controls, so a generated selector anchors to something that survives a re-render instead of a generated class name that churns.

This matters because it fixes the app's testability from the outside. You don't file a ticket to add data-testid attributes across a legacy screen; the plugin adds them at run time, every run, and they're gone the moment the test ends. The app ships unchanged. The tests get the stable hooks they'd have if someone had instrumented the UI by hand.

Layer 2: page-bound helper tools

The second layer is a set of helpers the generated test can call, bound to the live page under a namespace — for Wisej, page.wisej_net.<tool>(). These aren't wrappers around clicking pixels. They fire framework-event-path actions and, crucially, they read a widget's backing model where the DOM is lying.

The virtualized grid is the canonical case. Picture an ERP order screen with a grid of 8,000 rows, server-virtualized so only the visible ~20 are ever in the DOM. Scraping getByRole('row') sees those 20 and nothing else; the model returns placeholders for blocks the server hasn't sent. An agent without help burns a dozen turns screenshotting, scrolling by pixels, and guessing — or sets a filter and hits the classic "filter applied but the grid shows 0 rows" race because the refresh round-trip hasn't landed yet.

The plugin's grid helpers sidestep all of it by talking to the model the way the framework's own client runtime does:

// Know the grid first: true row count + column schema, not the DOM's lie
const info = await page.wisej_net.gridInfo({ id: "grdOrders" });
// info.rowCount === 8123, info.virtualized === true

// Search the FULL virtualized set, block by block — not just the viewport
const hit = await page.wisej_net.gridFindRow({
  id: "grdOrders",
  criteria: { Customer: "Acme Freight", Status: "Overdue" },
});
// hit.found === true, hit.row === 5170 (scrolled into view)

// Trusted double-click fires the real edit gesture on the located row
await page.wisej_net.gridDoubleClickCell({ id: "grdOrders", row: hit.row });

There's a companion for the refresh race, too: after a filter or sort, gridWaitForRows({ min: 1 }) awaits the async round-trip until rows actually materialize, so "0 rows so far" resolves honestly instead of failing a screenshot poll. The same shape covers other model-backed controls — combo boxes whose options are virtualized or grid-backed, tree views that hold collapsed branches only in the model, ribbon toolbars where toolbarButtonInfo maps an icon-only button to its tooltip before you try to click it.

The through-line: when the DOM is a partial, lossy projection of the real state, the helper reads the real state. That's not something browser-level automation can do on its own — it needs to know the framework.

Layer 3: generation guidance

The third layer never runs in your app at all. It's knowledge handed to the generator so it authors the right test the first time.

Two pieces. A skill prompt spells out the framework's selector rules — for Wisej, things like "scope selectors through the owning dialog, because hidden MDI pages keep their DOM and control names repeat across forms," and "never disambiguate with .first(); fix a strict-mode collision by scoping, not by position." And a per-control strategy map tells the generator how each widget actually wants to be driven. A Wisej ComboBox, for example, is driven by opening it then choosing the option — a plain select_option is a server-side no-op that silently does nothing. Without the map, the generator would author select_option, watch it fail, and spend turns figuring out why. With it, the first draft is already correct.

This is the layer that turns the other two from "available" into "used well." Helpers only help if the generator knows to reach for them; init-script test ids only help if the selector strategy expects them. The guidance closes that loop. If you want the fuller picture of how the generator turns a plain-language description into a verified spec, see how TestVibe generates tests.

The same surface carries non-framework skills

Here's the part that answers "will this work on my stack." The three-layer surface isn't Wisej-specific — Wisej is just the framework that happens to exercise all three layers hard. The same plugin mechanism carries skills that have nothing to do with rendering.

The MailSlurp plugin uses layer two — page-bound helpers — to pull a one-time passcode or magic-link out of a real inbox mid-test, so a login flow gated on email OTP runs end to end (walked through in testing email OTP with MailSlurp). The visual-regression plugin adds pixel-diff assertions alongside the functional ones, which is a different job entirely from driving a widget (the trade-offs there are in visual vs functional testing). Same install surface, same page.* binding convention, same generation-guidance hook — different capability. That's the point of a plugin system: the layers are a contract, and the contract is general. The full catalog and how enabling one changes generation is covered in plugins that extend your tests.

So the honest answer to the weird-framework question is layered too. The floor already drives most of what your framework renders, no plugin required. Where your framework hides state behind virtualization, buries controls the accessibility tree can't see, or demands a specific gesture to trigger a real event — that's exactly the shape a plugin exists to fill. And if there isn't one for your stack yet, the mechanism that would carry it is the same one shipping today.

Try it on your stack

The fastest way to find out where your app sits — floor, plugin, or a genuinely hard edge — is to point TestVibe at it and describe a flow that touches your gnarliest widget. Get early access and see how far the floor carries you before you ever reach for a plugin.

Early access

Ready for tests that write themselves?