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.
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.
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.
util.sleep can be useful while stabilizing a new flow, but over time it is better to rely on stronger URL or visibility-based assertions wherever possible.
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.

