Kaizen Teams

Dropdown

Table of Contents

Time to read

·

12

Published on

·

May 14, 2020

Last updated on

·

August 18, 2026

Pablo Marcano

Technology

Technology

Setting up Appium for React Native e2e - Automation Testing

Published on

·

August 18, 2026

Last updated on

·

August 18, 2026

Time to read

·

12

Pablo Marcano

End to End (e2e) testing is a technique that helps ensure the quality of mobile applications in an environment as close to real life as possible, testing the continuous integration of all the pieces that integrate a software automatically. On a mobile app, this could be particularly useful given the diversity of real devices and platforms our software is running on top of.

Due to the cross-platform nature of React Native, e2e testing proves to be particularly messy to work on. As a result, we have to write all of our tests bearing this in mind, changing the way we access to certain properties or query elements no matter the tool we use for connecting to it. Still, automation testing tools like Appium and WebdriverIO allow us to work over a common and somewhat standard interface.

The following instructions assume we already have React applications built with expo, and use Jest for our unit-testing solution.

Disclaimer: The following instructions are based on a Windows machine running an android emulator. output/commands may vary slightly on different architectures.

Setting Up Appium

  • Install required dependencies

$ npm i -D webdriverio babel-plugin-jsx-remove-data-test-id concurently

WebdriverIO will work as our “client” for the appium server in the case of JS. There is more to come regarding how to use other clients such as python.

babel-plugin-jsx-remove-data-test-id will help us remove unwanted accessibilityLabels from our mobile app, since that’s the preferred way of targeting elements for both IOS and Android platforms

concurrently will help us automate the running of appium server and jest to do our e2e tests

  • Install Appium Doctor

$ npm install appium-doctor -g

This will help us identify if we have all of the needed dependencies to correctly run appium in an emulator.

  • Run Appium Doctor

Depending on the host OS we want to test in, we could run:

$ appium-doctor --android

or

$ appium-doctor --ios

For this particular case I’ll be running the android version. This will prompt some output on the console. If we have all the required dependencies installed we should see a message similar to the following

Code Shot of Appium Doctor Messaging

If not all necessary dependencies are met at this point, instead of checkmarks before any given item you’ll see a red X symbol. Check the end of the input for more information on how to fix the particular Issues you’re prompted.

We’re not going to fix the optional requirements that appium-doctor prompts for the time being, feel free to go over those once you have the testing solution working.

  • Run Appium

By this point, you should be able to run your appium server without any issues, in order to do so just type

$ appium

You should see something similar to

Coding Screen of Appium Doctor Messaging

If you do so, congrats! you have correctly set up appium.

Now, let's set up our tests.

Write tests once, run in any platform

One of the key features of React Native is its ability to write code once and run it in both iOS and Android, that is what we want our mobile tests to behave in the same way. There are some limitations for this, since the only way we can write a selector for both platforms is through the accessibilityLabel attribute in React Native.

This may become an issue if your mobile app depends on accessibility features. Make sure to use correct, semantic and descriptive accessibility labels at any place you intend to use them.

If a great accessibility is not on the scope of your current project (it should), you can use accessibilityLabel as a perfect target for querying your elements, just make sure you don’t accidentally worsen the experience of people using screen readers or any other assistive technology.

In order to do this, we’re going to configure our babel setup to remove the accessibility labels whenever we build for production:

/// babel.config.js
module.exports = function() {
return {
presets: ['babel-preset-expo'],
env: {
production: {
plugins: [
[
'babel-plugin-jsx-remove-data-test-id',
{ attributes: 'accessibilityLabel' },
],
],
},
},
};
};

Let’s write our first test script now:

I’ve created a called LoginTest.spec.js inside a new folder called e2e. Inside the file you can find the following:

// myapp/e2e/LoginTest.spec.js
import wdio from 'webdriverio';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const opts = {
path: '/wd/hub/',
port: 4723,
capabilities: {
platformName: 'android',
deviceName: 'emulator-5554',
app: 'my-app-name.apk',
automationName: 'UiAutomator2',
},
};
describe('Expo test example', function() {
let client;
beforeAll(async function() {
client = await wdio.remote(opts);
await client.pause(3000);
const pack = await client.getCurrentPackage();
const activity = await client.getCurrentActivity();
await client.closeApp();
await client.startActivity(pack, activity); //Reload to force update
await client.pause(3000);
});
afterAll(async function() {
await client.deleteSession();
});
it('should allow us to input username', async function() {
// Arrange
const field = await client.$('~username');
const visible = await field.isDisplayed();
// Act
await field.addValue('testUsername');
// Assert
expect(visible).toBeTruthy();
expect(await field.getText()).toEqual('testUsername');
});
});

That may be a lot of new code to digest at once, so let’s go line by line:

import wdio from 'webdriverio';

First, we import the WebdriverIO client. This is the main package that will include the functionality we need to query elements from the react app and simulate events on the emulator.

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

This will tell our test runner (in this case jest) to make the tests error after a certain number of ms have passed. Here we’re setting it explicitly in the test, but if you’re using jest you can modify the testTimeout property on your jest configuration. If you’re using any other test runner, I’d recommend going through their documentation, most of them have a similar property.

const opts = {
path: '/wd/hub/',
port: 4723,
capabilities: {
platformName: 'android',
deviceName: 'emulator-5554',
app: 'my-app-name.apk',
automationName: 'UiAutomator2',
},
};

These are the configurations for our driver to know what to look for when using the appium interface to query and save elements.

You can get the device name going on your emulator > help > about

In order to generate an app from expo, you have to run the command:

expo build:android

And wait in the queue for it to build.

In this case, I placed the downloaded apk in the root folder for my project, and renamed it my-app-name.apk.

Since we’re using WebdriverIO, the automationName will be UiAutomator2, as that’s how appium recognizes it.

Since lines 18-33 are mostly about setup, we won’t focus on that for now. The next part focuses on line 34 and forward.

Writing the actual test

The idea of this test is just to showcase a normal flow on a test, therefore we will be dealing with a fairly simple use case: Checking that we have a valid username input:

const field = await client.$('~username');
const visible = await field.isDisplayed();

The first line allows us to query an item by accesibilityLabel. As I have previously mentioned, for more information about specific selectors go to the WebdriverIO documentation.

The second line checks whether our previously selected item is visible on the current screen, more information here.

await field.addValue('testUsername');

This line simulates user typing into the selected field. In this case, we’re inserting the ‘testUsername’ text inside the previously selected username field:

expect(visible).toBeTruthy();
expect(await field.getText()).toEqual('testUsername');

Lastly, we use Jest to check that the field is indeed visible on our Login Screen, and that the text on the given username field is the same as the one we wrote in it.

Running the test

Since we’re using Jest as our test runner on our React Native app, I’ve set up a command on my package.json to run the appium server and to run Jest in watch mode at the same time. It looks like this:

Screenshot of Command to Run Appium Server

Here we’re using concurrently, a simple npm package that allows us to run several npm scripts at the same time. In this case we run the appium server and jest in watch mode, add their names and different colors to easily recognize them in the console, and pass the standard input to the jest command. This way we can narrow down our tests or do things like run coverage reports.

With this done, we simply have to run npm run test:e2e on our console, and expect something like this:

Lines of code in appium

to be run, and something like this:

Lines of code

to be the output. If so, congratulations, you’ve correctly set up your integration tests for your react native app.

Wrapping up

While we’re far away from calling it a day on our e2e react app testing solution, the main automation testing setup it’s done. Next steps include integrating it with a CI/CD pipeline and making it work on IOS platforms.

Further Reading
https://webdriver.io/
https://discuss.appium.io/
http://appium.io/

End to End (e2e) testing is a technique that helps ensure the quality of mobile applications in an environment as close to real life as possible, testing the continuous integration of all the pieces that integrate a software automatically. On a mobile app, this could be particularly useful given the diversity of real devices and platforms our software is running on top of.

Due to the cross-platform nature of React Native, e2e testing proves to be particularly messy to work on. As a result, we have to write all of our tests bearing this in mind, changing the way we access to certain properties or query elements no matter the tool we use for connecting to it. Still, automation testing tools like Appium and WebdriverIO allow us to work over a common and somewhat standard interface.

The following instructions assume we already have React applications built with expo, and use Jest for our unit-testing solution.

Disclaimer: The following instructions are based on a Windows machine running an android emulator. output/commands may vary slightly on different architectures.

Setting Up Appium

  • Install required dependencies

$ npm i -D webdriverio babel-plugin-jsx-remove-data-test-id concurently

WebdriverIO will work as our “client” for the appium server in the case of JS. There is more to come regarding how to use other clients such as python.

babel-plugin-jsx-remove-data-test-id will help us remove unwanted accessibilityLabels from our mobile app, since that’s the preferred way of targeting elements for both IOS and Android platforms

concurrently will help us automate the running of appium server and jest to do our e2e tests

  • Install Appium Doctor

$ npm install appium-doctor -g

This will help us identify if we have all of the needed dependencies to correctly run appium in an emulator.

  • Run Appium Doctor

Depending on the host OS we want to test in, we could run:

$ appium-doctor --android

or

$ appium-doctor --ios

For this particular case I’ll be running the android version. This will prompt some output on the console. If we have all the required dependencies installed we should see a message similar to the following

Code Shot of Appium Doctor Messaging

If not all necessary dependencies are met at this point, instead of checkmarks before any given item you’ll see a red X symbol. Check the end of the input for more information on how to fix the particular Issues you’re prompted.

We’re not going to fix the optional requirements that appium-doctor prompts for the time being, feel free to go over those once you have the testing solution working.

  • Run Appium

By this point, you should be able to run your appium server without any issues, in order to do so just type

$ appium

You should see something similar to

Coding Screen of Appium Doctor Messaging

If you do so, congrats! you have correctly set up appium.

Now, let's set up our tests.

Write tests once, run in any platform

One of the key features of React Native is its ability to write code once and run it in both iOS and Android, that is what we want our mobile tests to behave in the same way. There are some limitations for this, since the only way we can write a selector for both platforms is through the accessibilityLabel attribute in React Native.

This may become an issue if your mobile app depends on accessibility features. Make sure to use correct, semantic and descriptive accessibility labels at any place you intend to use them.

If a great accessibility is not on the scope of your current project (it should), you can use accessibilityLabel as a perfect target for querying your elements, just make sure you don’t accidentally worsen the experience of people using screen readers or any other assistive technology.

In order to do this, we’re going to configure our babel setup to remove the accessibility labels whenever we build for production:

/// babel.config.js
module.exports = function() {
return {
presets: ['babel-preset-expo'],
env: {
production: {
plugins: [
[
'babel-plugin-jsx-remove-data-test-id',
{ attributes: 'accessibilityLabel' },
],
],
},
},
};
};

Let’s write our first test script now:

I’ve created a called LoginTest.spec.js inside a new folder called e2e. Inside the file you can find the following:

// myapp/e2e/LoginTest.spec.js
import wdio from 'webdriverio';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
const opts = {
path: '/wd/hub/',
port: 4723,
capabilities: {
platformName: 'android',
deviceName: 'emulator-5554',
app: 'my-app-name.apk',
automationName: 'UiAutomator2',
},
};
describe('Expo test example', function() {
let client;
beforeAll(async function() {
client = await wdio.remote(opts);
await client.pause(3000);
const pack = await client.getCurrentPackage();
const activity = await client.getCurrentActivity();
await client.closeApp();
await client.startActivity(pack, activity); //Reload to force update
await client.pause(3000);
});
afterAll(async function() {
await client.deleteSession();
});
it('should allow us to input username', async function() {
// Arrange
const field = await client.$('~username');
const visible = await field.isDisplayed();
// Act
await field.addValue('testUsername');
// Assert
expect(visible).toBeTruthy();
expect(await field.getText()).toEqual('testUsername');
});
});

That may be a lot of new code to digest at once, so let’s go line by line:

import wdio from 'webdriverio';

First, we import the WebdriverIO client. This is the main package that will include the functionality we need to query elements from the react app and simulate events on the emulator.

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

This will tell our test runner (in this case jest) to make the tests error after a certain number of ms have passed. Here we’re setting it explicitly in the test, but if you’re using jest you can modify the testTimeout property on your jest configuration. If you’re using any other test runner, I’d recommend going through their documentation, most of them have a similar property.

const opts = {
path: '/wd/hub/',
port: 4723,
capabilities: {
platformName: 'android',
deviceName: 'emulator-5554',
app: 'my-app-name.apk',
automationName: 'UiAutomator2',
},
};

These are the configurations for our driver to know what to look for when using the appium interface to query and save elements.

You can get the device name going on your emulator > help > about

In order to generate an app from expo, you have to run the command:

expo build:android

And wait in the queue for it to build.

In this case, I placed the downloaded apk in the root folder for my project, and renamed it my-app-name.apk.

Since we’re using WebdriverIO, the automationName will be UiAutomator2, as that’s how appium recognizes it.

Since lines 18-33 are mostly about setup, we won’t focus on that for now. The next part focuses on line 34 and forward.

Writing the actual test

The idea of this test is just to showcase a normal flow on a test, therefore we will be dealing with a fairly simple use case: Checking that we have a valid username input:

const field = await client.$('~username');
const visible = await field.isDisplayed();

The first line allows us to query an item by accesibilityLabel. As I have previously mentioned, for more information about specific selectors go to the WebdriverIO documentation.

The second line checks whether our previously selected item is visible on the current screen, more information here.

await field.addValue('testUsername');

This line simulates user typing into the selected field. In this case, we’re inserting the ‘testUsername’ text inside the previously selected username field:

expect(visible).toBeTruthy();
expect(await field.getText()).toEqual('testUsername');

Lastly, we use Jest to check that the field is indeed visible on our Login Screen, and that the text on the given username field is the same as the one we wrote in it.

Running the test

Since we’re using Jest as our test runner on our React Native app, I’ve set up a command on my package.json to run the appium server and to run Jest in watch mode at the same time. It looks like this:

Screenshot of Command to Run Appium Server

Here we’re using concurrently, a simple npm package that allows us to run several npm scripts at the same time. In this case we run the appium server and jest in watch mode, add their names and different colors to easily recognize them in the console, and pass the standard input to the jest command. This way we can narrow down our tests or do things like run coverage reports.

With this done, we simply have to run npm run test:e2e on our console, and expect something like this:

Lines of code in appium

to be run, and something like this:

Lines of code

to be the output. If so, congratulations, you’ve correctly set up your integration tests for your react native app.

Wrapping up

While we’re far away from calling it a day on our e2e react app testing solution, the main automation testing setup it’s done. Next steps include integrating it with a CI/CD pipeline and making it work on IOS platforms.

Further Reading
https://webdriver.io/
https://discuss.appium.io/
http://appium.io/

Related Articles

View all articles

·

Aug 14, 2026

Running Synthetic Users Into Claude Code

A synthetic user research framework, turned into a Claude Code plugin that runs automated UX tests with AI agents, step by step.

12 read time

Read more

A synthetic user is a constrained AI decision agent defined by twelve fields, from functional role and context to assumptions and abandonment rules.

In the previous post I built an early, working implementation, and the next question was whether the same rules could hold up in a repeatable, automated test.

This post is that next step: how I turned the framework into a Claude Code plugin, and the technical decisions behind adapting methods designed for people into something an AI can execute without cheating.

Why “find the usability issues” is not enough

Give a model a URL and ask it to “find the usability issues.” It works halfway. And the “halfway” is the interesting part, It gives you a generic list, correct in the abstract, useless in practice.

A usability issue matters because of who encounters it and under what conditions.

Using an app from bed is not the same as using it on a factory floor. Urgency changes, lighting changes, attention changes, previous knowledge changes. The same confusing button can be irrelevant to a power user and an abandonment point for an operator wearing gloves.

The whole design comes from that observation: the AI does not evaluate the interface. It acts as a specific person in front of the interface.

The person brings the context with them. And the context turns a list of defects into a list of priorities.

Anatomy of a simulation

An orchestrator controls the browser through Playwright MCP. It reads each screen as an accessibility snapshot: text, roles, states, no guessing pixels. Then it acts on specific elements.

The decision on each screen is made by an isolated subagent, which returns a JSON for each step:

{

  "action": "...",

  "clarityLevel": "High|Medium|Low",

  "doubtDetected": true,

  "reason": "...",

  "abandoned": false,

  "estimatedTimeSeconds": 40,

  "emotionalState": "...",

  "memory": "..."

}

Two rules make this look more like a person and less like an oracle.

1. The evaluator never sees the end.

The evaluator receives one screen at a time, without knowing how many are left or what comes next in the flow.

If the interface leaves room for a mistake, the synthetic user makes the mistake. It clicks where a person would click, not where it is convenient to click in order to complete the test. This is where the framework’s forbidden assumptions live. The agent cannot assume backend logic or mentally complete what the screen does not show.

2. Emotion is memory, not decoration.

The memory field travels from one step to the next. The emotional state is inherited and accumulates. A frustration +1 persists. This detects something that is structurally invisible to any test that evaluates screens separately.

Screen five does not necessarily fail because of screen five. It fails because the user gets there with accumulated frustration.

Evaluated alone, that screen passes. Evaluated by someone carrying three doubts and one broken promise, it triggers abandonment. In the first post, I wrote that doubt is not failure. It is the signal that reveals structural friction.

Emotional memory is that idea turned into architecture.

Eight subagents, one job each

Each subagent gets a clean context. It knows the minimum required to do its job.

That ignorance is deliberate.

The agent acting as the user does not know what the orchestrator knows. It cannot compensate for bad design with knowledge a real person would not have.

Subagent

What it does

Subagent What it does
synthetic-screen-evaluator Acts as the user on one screen and returns the JSON for that step
synthetic-flow-synthesizer Reads the complete run and writes the report. It never simulates again
synthetic-profile-generator Generates a complete profile from an approved spec, choosing from a controlled vocabulary
synthetic-autopilot-synthesizer Consolidates N runs and classifies findings by convergence across users
heuristic-persona-generator Creates the 3 persona raters based on the business being evaluated
heuristic-expert-evaluator Detects violations of the 10 heuristics using forced enumeration
heuristic-persona-rater Scores each finding from the experience of ONE persona. It runs ×3
heuristic-report-synthesizer Builds the final report using the already computed numbers

Adapting a human test: the heuristic evaluation

A textbook heuristic evaluation uses three to five human evaluators because each human finds different problems.

My first experiment was literal, and it went meh.

I iterated until I reached two synthetic detection runs with different agents, coverage was extremely high, but it exposed another problem: an unmanageable list. Dozens of valid issues, very few important ones.

The final design separates those two jobs.

1. An expert finds violations.

Based on Nielsen’s literature, an expert goes through each screen and is forced to produce a verdict for every heuristic: 

  • Violation
  • Clean
  • Not observable

Each verdict includes textual evidence from the snapshot, forced enumeration breaks the habit of reporting only the things that stand out.

2. Three synthetic personas decide what matters based on what they bring with them: context, emotions, urgency, and constraints.

Three synthetic personas are generated according to the business being evaluated: 

  • power user
  • average user
  • low digital literacy

They score the findings without seeing the expert’s conclusions. The same issue can matter very differently depending on what each persona brings to it.

The formula is business impact × usability impact, with agreement between personas as the tiebreaker.

This keeps issue detection and user impact as separate jobs: the expert identifies the violations, and the personas help determine which ones deserve attention first.

Three modes, and a tool for building users

The plugin currently has three modes.

simulation-run (custom)

You build a profile field by field in the Synthetic User Builder, the tool I built to materialize the framework.

First come the attributes: 

  • Role in relation to the product
  • Boundaries
  • Initial emotional state
  • Context
  • Forbidden assumption

Only after that, and separately, comes the task.

The profile describes how someone decides, never what they have to do. That is why the same profile can be reused across tests.

simulation-auto (inferred)

You only give it the URL.

It researches the business, infers the typical roles, proposes users with tasks, and you adjust that proposal in natural language before anything runs.

heuristic-test (inspection)

The heuristic test described above, for one screen, one flow, or the entire site.

Everything run becomes a file

Every run leaves Markdown artifacts inside the project:

user-simulation-tests/

├── simulation/

│   ├── profiles/    ← users: the .md used for simulation + a .builder.json

│   │                   that can be imported back into the Builder and edited manually

│   └── results/     ← one report per run + the consolidated report from auto mode

└── heuristic/

    ├── personas/    ← the 3 raters + business research, reused across runs

    └── results/     ← reports with the prioritized findings table

Simulation reports include the full step by step flow, the emotional arc, risks, and a single “Fix this first.”

The consolidated report classifies findings by convergence: did one user suffer from this, or did all of them?

The decision to keep everything as accumulating .md files is strategic.

These are different runs, using different lenses, that can be analyzed together later, crossing heuristic violations with simulated emotions answers something no individual test gives us:

Of everything that is wrong, what actually matters?

Models and costs

What worked for me for the synthesis subagents:

  • For reports, consolidation, and the heuristic expert, the best available model makes sense. That is where the judgment lives.
  • For the screen evaluator, a medium and fast model is enough. There are many short, constrained calls, and the profile already restricts the decision.
  • The raters are the lightest case.

A complete run consumes between 100k and 400k tokens, depending on the model and mode, in around 20 minutes.

That is the cost of a test that previously required coordinating the schedules of three professionals, and that can now run against every iteration of the product.

See it in action

Here's a complete run against our site, kzsoftworks.com: a skeptical "Business Leader" profile, five live browser steps, and a full Markdown audit in under three minutes that names the exact moment the executive persona lost trust.

It is still early, but it already runs

Every rule in the framework became an architectural constraint: clean context, one screen at a time, emotional memory, forbidden assumptions.

The plugin is open source: github.com/PabloManzoni/user-simulation.

Three commands, and the inferred mode only needs your URL.

If you try it and your synthetic user abandons on screen three, you already know what it means:

It is not failure. It is the signal.

·

Aug 14, 2026

Generative UI: How to keep the experience under control

Generative UI can adapt interfaces to each user, but it adds risks around reliability, latency, cost, security, and accessibility. Learn the architecture that keeps those risks under control.

12 read time

Read more

Generative UI assembles the interface around what each user is trying to do, instead of showing everyone the same fixed screen. That flexibility comes with real considerations: keeping the experience consistent, secure, and easy to support once it's live. This post covers what generative UI is worth building for, what it costs, and how teams keep it under control.

Generative UI works best when the experience is dynamic, but the system behind it stays tightly controlled.

Start by defining which parts of the interface can change, which cannot, and what must be validated before anything reaches the user.

TL;DR

  • Interfaces can adapt to user context, support more variations without designing every screen by hand, and reduce unnecessary steps in a workflow.
  • The trade-offs include inconsistent experiences, unreliable or unsafe output, added latency and infrastructure cost, and harder analytics and debugging.
  • Better prompting can reduce unwanted behavior, but it cannot guarantee reliability, security, or consistency. Those controls need to exist around the model: a stable interface shell, a closed component catalog, validation of model output, session-level logging, and model routing with fallback options.
  • Every control introduces a trade-off. No architecture maximizes flexibility, reliability, privacy, performance, and cost at the same time.

What does generative UI make possible?

Interfaces that adapt to context

The interface can adapt to what a person is trying to do instead of relying only on a persona defined at design time. Steps can reorder or disappear based on intent. It can change how much information it shows and what it emphasizes. Copy can adapt to the user's locale and context instead of relying on literal translation.

More interface variations with less custom development

A small set of components can support many variations without designing each screen separately. The system can also support workflows the team did not design as individual screens, as long as the required components and actions already exist.

Fewer steps between intent and action

The interface can hide controls a task does not need, reducing the number of steps required to complete it. Generative UI can also help teams test different ways of presenting the same task. Whether that improves completion or conversion depends on the workflow.

What can go wrong with generative UI?

Experience consistency risks

When layouts change between users or sessions, they can break muscle memory and make support harder. They can also drift from the design system or disrupt accessibility patterns that depend on consistent structure.

Reliability and security risks

The system should not trust model output by default. A model can render a button that does nothing, display fabricated data in a component, or produce a state the team never tested. Prompt injection can push it toward components, content, or actions the system should not allow. Weak controls can expose sensitive data or allow actions and interface states the product should block.

Performance and infrastructure risks

A generative interface also inherits the model layer's latency, cost, and availability risks. Waiting on an LLM to generate a layout adds delay before a page renders. Each generation uses processing resources, and hosted models usually add usage-based cost. Relying on one provider also exposes your product to outages, API changes, price increases, and deprecations.

Analytics and debugging risks

Standard analytics often assume a fixed set of screens. Heatmaps and funnels become harder to compare when users see different layouts. Reproducing a bug also gets harder when you cannot reopen the exact screen the user saw.

How do you control these risks?

Prompts can reduce unwanted behavior, but they cannot enforce which components the system may render or which actions it may allow. Those limits need to be enforced in the architecture around the model.

What parts of a generative interface should remain fixed?

Keep global navigation, account and security controls, primary actions, critical transaction controls, and accessibility-critical structure fixed. Let the model modify only the content and controls that benefit from adaptation.

Fixed navigation preserves familiar interaction patterns. A stable structure also makes accessibility testing, branding, and support more predictable.

How do you stop generative UI from creating broken interfaces?

Do not let the model generate arbitrary UI code. Have it return structured configuration instead. The schema should specify the component, its data, and its position. Validate that output against a closed catalog before rendering it.

The model should not write HTML, CSS, or JavaScript or choose anything outside that catalog. This reduces invalid layouts and unsupported combinations. This is the declarative approach we covered in Part 1.

How should teams test and secure generative UI?

Treat model output as untrusted input. Validate it against the schema and component allowlist, sanitize content, and keep authorization outside the model.

Add content security policies and prompt-injection defenses based on what the model can access and what actions it can trigger. Pay particular attention to user-provided content, privileged actions, sensitive data, and external tools.

Limit valid component combinations, then use visual regression and property-based tests to exercise unexpected inputs and edge cases.

Minimize sensitive data sent to the model. Mask or anonymize it before generation when the task does not require the original values.

How do you monitor a UI that looks different for every user?

Record enough context to reconstruct each generated interface. That includes detected intent, model version, generated configuration, rendered components, task completion, and errors, all tied to the session.

That record lets teams segment analytics by generated experience and reconstruct what a user saw during a specific session.

How do you control latency, cost, and outages?

Cache reusable results where freshness and privacy allow. Show a skeleton layout immediately and stream the rest in. Route simpler requests to smaller or local models, and reserve larger ones for complex requests. Put providers behind the same integration layer so you can switch models or fall back to a static experience during an outage.

What it controls Risks it mitigates
Stable interface shell Keeps navigation, account controls, and primary actions fixed Muscle memory loss, brand drift, accessibility gaps, support friction
Component-based UI Model outputs configuration, not code UI hallucinations, broken layouts, brand inconsistency, testing complexity
Untrusted-input handling Schema validation, allowlists, sanitization, sensitive-data controls Prompt injection, unsafe states, fabricated actions, privacy exposure
Session-level logging Records intent, generated configuration, rendered components, and outcome Fragmented analytics, hard-to-reproduce bugs, support friction
Model routing and fallback Caching, streaming, model routing, provider switching Latency, model cost, provider downtime, difficulty switching providers

What do these controls cost you?

Keeping more of the interface fixed protects consistency but limits personalization. Limiting combinations makes the system easier to test but reduces how much it can vary. Caching lowers cost, but cached output can go stale.

Running models locally can reduce how much sensitive data leaves your infrastructure, but it adds systems your team has to operate and maintain. Detailed session logs can make support easier, but they also create storage, retention, and privacy requirements.

No architecture maximizes flexibility, reliability, privacy, performance, and cost at once. You need to decide which trade-offs matter most for each workflow and design around them.

llms.txt