UI Testing
Playwright-based frontend E2E testing support in SolidX, including navigation, actions, assertions, and runtime expectations.
Mental Model
UI testing in SolidX is browser automation inside the shared metadata-driven engine, not a separate testing stack.
- Scenarios still live in
testing.scenarios. - Interpolation,
saveAs, and shared runtime state still work the same way. - Playwright is the execution adapter behind the UI operations.
mixedscenarios let API setup and UI verification work together cleanly.
SolidX supports frontend end-to-end testing through a Playwright-based UI adapter.
This lets browser-level automation run through the same metadata-driven testing engine used for API testing.
Prerequisites
UI scenarios need a Chromium build. SolidX fetches it for you, so there is no manual setup step.
The first test run that selects a UI scenario downloads Chromium (around 150MB) before the run starts, then reuses it on every later run. Runs filtered to API-only scenarios never trigger the download, and neither do projects without UI scenarios — so trying SolidX out costs you nothing until you actually write a UI test.
Applications running Test Hub do not wait for that first run. Test Hub warms the browser up as it starts, so the download is usually finished long before anyone triggers a run. Installing Test Hub is itself the signal that you intend to run UI tests, so there is nothing to configure.
That warm-up never blocks startup — the app boots and serves requests while the download proceeds in the background, and stopping the app cancels a download still in progress. If the warm-up fails, it is logged as a warning and the browser is fetched on the first UI run instead.
Nothing is downloaded at install time
npm install never fetches a browser. An application without Test Hub that never runs UI scenarios will not download Chromium at all.
Docker
Browsers install to ~/.cache/ms-playwright on Linux and ~/Library/Caches/ms-playwright on macOS. A multi-stage build that installs as root but runs the app as another user will not find the binary at runtime. Set PLAYWRIGHT_BROWSERS_PATH to a shared location in both stages to avoid this.
How UI Testing Works
At runtime:
- The runner loads
uiormixedscenarios from metadata. - It determines whether a browser is needed.
- The Playwright adapter is started when required.
- UI steps are executed through the adapter.
- The browser is shut down cleanly at the end of the run.
This means UI testing is integrated into the same engine rather than being maintained as a separate testing framework.
What UI Testing Is Best For
Use UI testing when you need to validate:
- Real user flows.
- Form behavior.
- Page transitions.
- Visible content.
- Routing outcomes.
- Browser-side interactions.
Runtime Expectations
UI testing requires a running frontend application.
That usually means supplying:
--ui-base-url- Optionally
--headless
solidctl test run --module venue --ui-base-url http://localhost:5173 --headless falseWithin scenarios, the UI base URL is available as ${env:TEST_UI_BASE_URL}.
If a scenario includes UI execution, the browser lifecycle is managed by the test runner.
UI steps wait up to 30 seconds for an element before failing. If that is not right for your application, see Timeouts and Waiting for how to change it.
Core UI Operations
Navigation
Common navigation primitives include:
ui.gotoui.expectUrl
Use them to open pages and confirm that routing behaved as expected.
Form Input
Common form primitives include:
ui.fillui.selectui.press
These are useful for login flows, search flows, forms, and interactive field-based scenarios.
Form Fields
Form inputs in SolidX get their id from the field's name in model metadata.
That means the selector for any form field is #fieldName, where fieldName matches the field's name property in the module metadata.
For example, a model with a field named title will render an input with id="title", so the selector is #title.
When writing form-level UI scenarios, look up the model's fields in metadata to find the right names.
Actions
The main action primitive is:
ui.click
This is commonly used for submit buttons, links, menu actions, and modal interactions.
Assertions
Common UI assertions include:
ui.expectVisibleui.expectTextui.expectUrl
These let you verify that the browser is showing the expected outcome of a user flow.
Example UI Flow
{
"id": "ui-login-happy-path",
"type": "ui",
"tags": ["smoke"],
"steps": [
{
"given": {
"op": "ui.goto",
"with": { "url": "${env:TEST_UI_BASE_URL}/auth/login" }
}
},
{
"and": {
"op": "ui.expectVisible",
"with": { "selector": "#identifier" }
}
},
{
"when": {
"op": "ui.fill",
"with": { "selector": "#identifier", "value": "libTestEditor@test.local" }
}
},
{
"and": {
"op": "ui.expectVisible",
"with": { "selector": "input[type='password']" }
}
},
{
"and": {
"op": "ui.fill",
"with": { "selector": "input[type='password']", "value": "Test@1234" }
}
},
{
"and": {
"op": "ui.click",
"with": { "selector": "button:has-text('Sign In')" }
}
},
{
"then": {
"op": "ui.expectVisible",
"with": { "selector": ".solid-admin-header" }
}
},
{
"and": {
"op": "ui.expectUrl",
"with": { "contains": "/admin" }
}
}
]
}Recommended reading of this flow:
- Navigate to
/auth/login. - Assert
#identifieris visible before filling it. - Fill the identifier field with the test user's email.
- Assert the password input is visible, then fill it. Use
input[type='password'], not#password. - Click the submit button by label with
button:has-text('Sign In'). - Assert
.solid-admin-headeris visible to confirm the app has loaded. - Assert the URL contains
/admin.
UI Testing Patterns
Common patterns include:
- Login and authentication verification.
- Create and edit flows through forms.
- Route guards and redirect behavior.
- Visibility of important dashboard content.
- Smoke checks for critical pages.
Practical patterns from real scenarios:
- Use
ui.expectVisiblebefore filling any input to confirm the element is ready. - Use
#identifierfor the login identifier andinput[type='password']for the password field. - Use
button:has-text('Sign In')for the login submit; it is more readable than a class selector. - Assert
.solid-admin-headervisibility after login before asserting the URL. - Use
.solid-sidebar-tree-link:has(.solid-sidebar-tree-label:text('ModelName'))to click a sidebar model link. - Assert
table tbodyvisibility to confirm a list view has loaded.
Avoid util.sleep for waiting. UI steps already wait for elements on their own, so a fixed delay only slows the run down. If a step needs more time, raise its timeoutMs instead — see Timeouts and Waiting.
Mixed Scenarios
One of the strengths of the SolidX testing model is that UI testing can be combined with API testing in mixed scenarios.
This is useful when:
- API setup is faster than doing the same setup through the browser.
- UI verification is still required at the end.
- A workflow naturally crosses backend and frontend layers.
For example:
- Create prerequisite data via API.
- Open a UI page.
- Assert that the created data is visible in the browser.
Headless vs Headed
Use headless mode when:
- Running in CI.
- You want fast non-visual execution.
Use headed mode when:
- Debugging a failing scenario.
- Developing a new UI scenario.
- Inspecting selectors and interaction timing.
Good UI Testing Practices
Recommended practices:
- Keep UI scenarios focused on user-visible behavior.
- Avoid using UI tests when API tests would cover the same risk more cheaply.
- Prefer stable selectors and predictable page states.
- Use test data and API setup steps to reduce unnecessary browser work.
- Use
mixedscenarios when that better reflects the real workflow. - Prefer visible-state assertions over arbitrary waits when possible.
- Keep login scenarios reusable, because they are often the first UI smoke test a module maintains.
Selector Conventions
SolidX UI components do not use data-testid. Selectors usually fall into three groups.
ID Selectors
Form inputs get their id from the field's name in model metadata.
#title -> input for a field named "title"
#identifier -> login identifier input (always this value)
#email -> email input on dedicated email formsWhen writing tests for a specific model's form, look up the model's fields[*].name values in the metadata to get the correct IDs.
CSS Class Selectors
The project uses stable solid-* BEM-style classes:
.auth-container login page container
.solid-admin-header top app header after login
.solid-sidebar navigation sidebar
.solid-data-table-row table rows in list views
.solid-table-paginator list view pagination bar
.solid-form-section form content wrapperPlaywright Extended Selectors
Use Playwright's extended syntax for buttons, menu items, and toasts:
button:has-text('Sign In') button by visible label
:text-is('Catalog') exact text match
:has-text('...') subtree text match
.solid-sidebar-tree-link:has(.solid-sidebar-tree-label:text('Book'))
div[role='status']:has-text('Invalid Credentials')Prefer button:has-text(...) over class-based button selectors when the button label is stable. It reads clearly and survives class renames.
Relationship To Playwright
Playwright is the execution adapter for UI automation in SolidX, but the testing model itself remains SolidX-native:
- Scenarios are metadata-driven.
- Step execution goes through the shared engine.
- Reporting and interpolation work the same way.
- Only the browser interaction layer is Playwright-specific.
That gives teams the power of Playwright without giving up the shared SolidX testing architecture.

