A Wisej.NET app looks like a web page and behaves like a desktop one. That gap is exactly where generic end-to-end tools fall apart. The DOM you see in the inspector isn't the source of truth — it's a projection of server-side control state, streamed down and patched in place. A click isn't a DOM event you fire and move on from; it's a round trip that mutates state on the server and pushes the result back. Test a Wisej.NET app as if it were static HTML and you'll spend your afternoon debugging failures that aren't bugs.
This is a guide to what actually breaks, why, and how to test these apps without fighting the framework.
Why generic selectors struggle
Point Selenium, Cypress, or hand-written Playwright at a Wisej screen and you hit the same four walls, in roughly this order.
A click is a round trip. In a plain SPA a button click runs a local handler and re-renders. In Wisej the click posts to the server, the server updates the control tree, and a patch comes back over the wire. If your test clicks and immediately asserts, you're racing the round trip — and a fixed waitForTimeout is a bet you lose the moment CI gets slow or the backend deploy adds 200ms. You have to wait on the result landing, not on a duration.
Hidden MDI pages keep their DOM, so names repeat. Wisej doesn't tear down MDI child forms when you switch away from them — it hides them and keeps their DOM parked in the document. Open three forms that each have a "Save" button or a control named txtName, and a naked getByRole('button', { name: 'Save' }) now matches three elements. Playwright's strict mode throws, and the tempting fix — .first() — is a coin flip that silently targets whichever form happens to be first in the DOM.
Ribbon buttons are icon-only. A lot of Wisej line-of-business UIs lean on toolbars and ribbons where the buttons are pure icons — no visible text, no accessible name a getByRole can key on. There's nothing for a text-based locator to grab.
The names are designer names. Wisej controls carry their designer identity — txtUsername, luConfirmedFilter, grdOrders — not the human label a user reads. Anchor a locator to getByRole('textbox', { name: 'txtUsername' }) and you've bound your test to an internal name that means nothing to the user and changes when a developer renames the field. That's the same brittleness that makes hand-written selectors flake everywhere, just with a Wisej accent — the general fix is the same discipline covered in resilient locators.
None of these are exotic. They're the default behavior of every non-trivial Wisej app. A tool that doesn't know it's looking at Wisej treats all four as your problem.
The answer: a framework-aware plugin
TestVibe ships a first-party Wisej.NET plugin, built by Ice Tea Group, the company that makes Wisej.NET. TestVibe itself is a Wisej application, so the plugin is dogfooded against our own product every day. The people who know exactly how a DataGridView virtualizes rows or how a ComboBox dispatches its selection event wrote the test helpers for them.
Enable the plugin on a project and generated Playwright tests can call helpers under page.wisej_net. Three things change.
Dialog-scoped selectors, automatically. The plugin's automationSnapshot enumerates the visible controls with their roles, labels, and — critically — a scoped locator that reaches through the owning dialog. Instead of an ambiguous getByTestId('save'), generation emits page.getByRole('dialog', { name: 'Edit shift' }).getByTestId('save'). The repeated-name problem from hidden MDI pages disappears because every locator is anchored to the form that owns it. Scoping, never positional .first(), is the rule.
Repaired ARIA and stable test IDs. The plugin ships an init script that runs in the page at test time. It patches the missing ARIA metadata Wisej controls don't emit on their own and stamps stable test ids onto them — so the accessible structure your test relies on actually exists, rather than being something you hope the framework rendered.
Trusted framework-event helpers. This is the part generic tools can't fake. A Wisej ComboBox is not an HTML <select> — calling Playwright's selectOption on it is a server-side no-op, because the real selection travels through the framework's open-button-then-option event path. The plugin carries that strategy per control type: comboboxSelectItem opens the dropdown and picks the option the way the runtime does; dateTimePickerSetValue, treeExpandItem, and the grid helpers all fire trusted framework events instead of poking the DOM and praying. Generation reaches for the right helper without you specifying it.
The virtualized grid, where DOM scraping lies
The sharpest example is a virtualized DataGridView. Server-side row virtualization means the grid keeps only the rendered viewport in the DOM — maybe 20 rows out of several thousand. Scrape getByRole('row') and you see those 20 and nothing else; ask the client model for a row the server hasn't sent yet and you get an empty placeholder back. That's the source of two classic time-sinks: an agent burning turns screenshotting and scrolling by pixels hunting for a row, and the "filters set but the grid shows 0 rows" race where you assert before the refresh round trip lands.
The grid helpers read values straight off the backing table model and force-and-await the server's row fetch the way the Wisej client runtime does. Take a workforce-scheduling app with a virtualized shift grid — here's opening one person's record for edit when their row sits far below the viewport:
// Know the grid before you touch it — true row count, schema, virtualization flag
const info = await page.wisej_net.gridInfo({ id: "grdShifts" });
// info.rowCount === 842, info.virtualized === true → the DOM is lying
// Search the FULL virtualized set block-by-block, not just the rendered viewport
const hit = await page.wisej_net.gridFindRow({
id: "grdShifts",
criteria: { Employee: "Reyes, Ana", Shift: "Night" },
});
// hit.found === true, hit.row === 517, already scrolled into view
// Trusted double-click — the framework's open-for-edit gesture, not a raw DOM click
await page.wisej_net.gridDoubleClickCell({ id: "grdShifts", row: hit.row });
Three calls, zero pixel-scrolling. gridFindRow iterates the server model, forcing each block fetch, so it finds row 517 even though it was never rendered. gridDoubleClickCell fires the real open-for-edit gesture. And after you've set a filter, gridWaitForRows({ id: "grdShifts", min: 1 }) awaits the async round trip before you read — resolving the "0 rows so far" race honestly instead of screenshot-polling until it happens to be true.
The same shape covers the rest of the control surface: comboboxGetItems reads a virtualized dropdown's items from its model including rows not in the DOM, treeGetItems reads collapsed branches the tree never mounted, and toolbarButtonInfo maps icon-only ribbon buttons to their tooltips so generation can pick the right one before clicking.
You describe behavior, not selectors
You don't write any of the above by hand. You describe the behavior in plain language — "open the night shift for Ana Reyes and change her start time" — and TestVibe generates the Playwright spec, wiring in the Wisej helpers where they're needed. Every generated test is verified by actually running it against your live app in an isolated cloud sandbox before you ever see it; a test that raced a round trip or targeted a placeholder row wouldn't survive that check. If you want the mechanics, how TestVibe generates tests walks through the pipeline; the same discipline — waiting on conditions rather than sleeping — is behind web-first assertions. The Wisej plugin is one of several framework and capability packs you can enable per project; plugins that extend your tests covers the rest.
The point isn't that Wisej apps are hard to test. It's that they're differently hard — the state lives on the server, the DOM is a partial mirror, and the events that matter travel a path a generic click can't reproduce. Test them with a tool that knows that, and the false failures stop.
Try it on your Wisej app
Point TestVibe at a Wisej.NET screen — your own app or demo.wisej.net — enable the Wisej.NET plugin, and describe a flow through one of your busy grid or MDI forms. You'll get a real, verified Playwright test back without hand-writing a single scoped locator.
Get early access and put it against the form you dread testing most.