SolidX
ReferenceTesting

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.
  • mixed scenarios 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:

  1. The runner loads ui or mixed scenarios from metadata.
  2. It determines whether a browser is needed.
  3. The Playwright adapter is started when required.
  4. UI steps are executed through the adapter.
  5. 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 false

Within 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

Common navigation primitives include:

  • ui.goto
  • ui.expectUrl

Use them to open pages and confirm that routing behaved as expected.

Form Input

Common form primitives include:

  • ui.fill
  • ui.select
  • ui.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.expectVisible
  • ui.expectText
  • ui.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 #identifier is 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-header is 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.expectVisible before filling any input to confirm the element is ready.
  • Use #identifier for the login identifier and input[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-header visibility 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 tbody visibility 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 mixed scenarios 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 forms

When 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 wrapper

Playwright 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.