When you ask an agent to build a feature, it may finish without writing or running tests, unless you ask for them. For an application with a graphical interface, ask it to test what the user actually does as well. We'll see how a tool called Playwright opens a page and tests an interaction. Then we'll look at how the application itself is structured, because some structures make testing much easier. When the logic is kept apart from the interface, most of it can be tested directly, without drawing any screen. That separation has a second benefit. The logic no longer depends on the interface toolkit it happens to use, so either side can be updated or replaced without rewriting the other. We'll use a design called Model View ViewModel to show how this works.
Playwright lets a test control a real browser. The test tells Playwright what to do, and Playwright opens the page in the browser, clicks, types, and reads what is on the screen.
Here is the page we will test. It is a small to-do app, and next to it is the HTML behind it. There is a box for a new item with the label New item, an Add button, and a list, which starts empty.
The label identifies the New item box. So the test can ask Playwright for the box with the label New item.
The button has the name Add. So the test can ask for the button named Add. The test uses these labels and names to find the controls it needs, the same names a person reads on the screen. Giving controls meaningful labels makes these tests easier to write.
Now here is a complete browser test for our to-do app, in Python. The app is running on this computer, and its list starts empty. The page the test receives is a browser page that Playwright supplies. The first step opens the app's address in that page.
Next, the test finds the New item box by its label and types Buy milk into it.
Then it finds the Add button and clicks it. On the page, Buy milk now appears in the list.
The last line checks that the list contains exactly one item, Buy milk. Playwright waits for that result, up to a timeout, before it gives up. This final check is what makes it a test. Clicking successfully is not enough. If the item never appears, the test fails, and the agent can read that failure.
Learning Playwright from scratch takes some work, and you do not have to do it all yourself. You can ask the agent to write and run these tests for you. When a feature depends on what a user does in the browser, ask explicitly for Playwright tests that check those interactions. What you need to specify is the action and the result you expect.
Why ask for a browser test at all, when we could test the code that adds an item? Let's look at a case where the difference matters. The to-do app has a function that adds an item to the list. A logic test calls that function directly, with no page, and checks the list it returns. That test passes.
Now suppose the add function is still correct, but the Add button is no longer connected to it. The logic test still passes, because it never touches the button. A user clicks Add, and nothing happens.
Our browser test catches this, because it goes through the real button and checks the result. It clicks Add, waits for Buy milk to appear, and fails with an empty list. This is the actual output from running that test against a page with the button disconnected.
That is what a browser test adds. It checks the interaction as well as the logic. This is why you should explicitly ask the agent to include GUI tests. They cover behaviour that tests of functions alone never exercise.
That browser test tells us whether the interface works. Now we want to test the logic without opening the page, and that depends on how the app is organized. We'll leave the to-do app here and use a smaller example, a temperature converter. The user enters a temperature in Celsius, clicks Convert, and sees the result in Fahrenheit. Twenty degrees Celsius becomes sixty-eight degrees Fahrenheit.
Even this small converter has three jobs. The first is the calculation itself, which works on a temperature as a number. The second job holds what the screen needs, as plain values in the program. There is a string with the text typed so far, a Convert action that runs the conversion, a string with the result to display, and an error message when the input is wrong. The third job is the controls themselves. The input box displays that text and lets the user change it. The button runs the Convert action when it is clicked. A label shows the result or the error. These controls exist only when the screen is drawn. The values in the second job can be set and checked without drawing anything.
The calculation accepts a number. The screen also has to deal with text someone is still typing, including text that is not a number, and decide what message to show. Those are different responsibilities. Keeping them separate lets us test the first two without creating any controls.
This way of organizing an app is called Model View ViewModel, or M-V-V-M. The three names are quite similar, so let's connect each one to its job in the converter.
The model holds the application's data and rules, which here means the function that converts Celsius to Fahrenheit. That's model in the usual software sense, not an AI model.
The view is what the user sees and interacts with, the input box, the button and the result. These are widgets drawn on the screen. Here the user has typed a-b-c into the input box, and a label shows the error.
The view model holds the values and actions that the view needs. Think of it as a model of the screen's state and of the actions available on it. It is not the screen itself, and it does not touch the controls. When the user types a-b-c, the view model holds that text as a string. It also holds the error message, Enter a number, and an empty result.
Why have a view model at all? A temperature as a number belongs to the calculation. But the text a user has typed, such as a-b-c, the error message about it, and the formatted result all belong to the screen interaction. The view model is where those live.
Now the connections. The view passes the user's input to the view model and invokes its commands, such as Convert. The view model uses the model to do the calculation. And the view displays the values that the view model exposes. The link between the controls and the view model's values and commands is called binding. Binding lets typing in the input box update a value, and lets a new result appear on the screen. So the view model works with values, not with a text box on a page.
Let's see how these three parts still produce one working converter. We'll follow a single interaction. In the view, the user types twenty and clicks Convert. The view passes the text and the Convert command to the view model, so the view model's text is now the string twenty.
The view model reads its input text as the number twenty, and asks the model to convert it.
The model returns the number sixty-eight.
The view model prepares the result text, sixty-eight degrees Fahrenheit, with no error. The view displays it through its binding to that value. Notice that the calculation never needs to know which control shows its answer.
Here is why this structure helps us test. A test can do what the view did, with no page open. It sets the view model's text, invokes Convert, and checks the text the screen would show. Here the typed text is twenty with spaces around it. The test checks that the view model ignores the spaces, shows no error, and sets the result text to sixty-eight degrees Fahrenheit, with its unit. We can also change the layout of the screen without changing the conversion rule. We still keep the browser test, to check that the real controls invoke the command and display its result.
A second test covers bad input. If the input text is a-b-c, invoking Convert should set the error to Enter a number, and clear any old result.
So which test catches which failure in the converter? Start with the model. We can check the calculation directly, for example that zero degrees Celsius converts to thirty-two Fahrenheit. What is left for a GUI test is that the user can reach that calculation through the page.
Next, the view model. We can check directly that the input a-b-c sets an error and clears the result. What is left for a GUI test is that the error is actually displayed on the screen.
The view and its connections have no direct check. Only a GUI test shows that clicking Convert runs the command and displays the result.
The direct checks can cover many cases without drawing the interface, and that matters because a GUI test costs more to run. It needs the app running, its screen drawn, and controls ready to use. For a browser test, that means a running browser and a loaded page. A small calculation check can take less than a millisecond, while a GUI test can take a few seconds. These are illustrative timings. Cost here means time and computing resources. The agent can do the work of writing a GUI test, but running it still means starting the app and drawing its screen.
So test the many calculation cases directly, and keep GUI tests for the interactions that need them. The converter is small, but the same separation pays off when the calculation becomes more substantial and the interface keeps changing. A tiny utility does not need three parts. The design earns its place when separating the jobs makes testing and changes easier.
Here is what you can ask the agent to do. First, ask it to propose a separation between the app's rules, the screen's values and actions, and the visible controls, using M-V-V-M where it helps. Ask it to explain the responsibilities before it changes any code. Two questions tell you whether the separation works. Can the rules run without a screen? And can the screen display a state it is given? Ask for a small separation that answers yes to both, not a rewrite that only gives files new names.
Second, ask it to add and run tests for the rules and the screen behaviour, and GUI tests for the main interactions and what they show. For a web app, that means Playwright. Playwright tests a GUI through the browser, and it is one kind of GUI testing tool. Desktop and mobile apps have their own testing libraries, and the idea is the same. The test performs the user's actions and checks what appears. Ask it to report the commands it ran, the results, and any tests it could not run.
You do not need to write every line of these tests yourself to ask for this. Name the interactions and the results you expect, then review the tests and their results. If the agent cannot run a GUI test, ask it to explain what is missing. A test file the agent has written is not evidence that the test passed.
You can now ask the agent to test the interface as well as the functions behind it. GUI tests perform the user's actions and check the result. Separating the rules and the screen's state lets many more cases be tested directly, while GUI tests check that the application works through its controls. Ask for both, and check what the agent actually ran.