I often plan large-scale UI code as three fundamental levels:
1. Model data
2. View data
3. Rendered data
The model data comes from whatever underlying data store and business rules you are working with.
The view data contains every value you need to render your UI. This typically includes a lot of data taken straight from the model. However, it can also include derived data, perhaps the result of some arithmetic calculation or the outcome of some conditional logic. This is also the level where any view metadata that isn’t persisted in the model lives, for example if you need to keep track of a cursor position, zoom level, contents of a clipboard/kill ring, etc.
The rendered data level only applies if you’re creating your UI using a descriptive/declarative system rather than by calling some sort of API. For example, for a web app, the rendered data would be HTML ready to put into the DOM. At this level, I try to keep the logic trivial; you might need some basic conditional or loop logic in the rendering, but the view data is where anything complicated happens, and the rendering logic in something like an HTML template shouldn’t be more complicated than “if (simple boolean value provided by view data) then (version A) else (version B)” or “for each item in (list provided by view data) do (render individual item)”.
Ideally, the conversions from model data to view data and from view data to rendered data are pure (as in without side-effects) functions, and therefore in principle they are amenable to automated testing techniques in isolation. For example, it is straightforward to add unit tests that calculated view data do have the expected values for sample model data.
In practice, I find automated testing of the view data to rendered data stage has very little value, for two reasons. Firstly, if your rendering stage is basically just filling out templates using view data and the logic is trivial, errors tend to be obvious: a table has no contents, for example, or an entire part of the page disappears. Secondly, my experience is that most of the bugs I find at the rendering level don’t actually originate in the rendering logic. Rather, they tend to be in some accompanying data, such as a CSS stylesheet that didn’t include the right prefixes or got the media queries wrong, or as a bug in the rendering engine itself that is out of your immediate control, such as a browser layout engine bug.
Edit: Of course, the above only describes data going one way through the system. Depending on the application, you might also have interactions that update view meta-data, and in anything beyond pure visualisation code you’ll surely have interactions that need to update the model data. This is also amendable to automated testing, as long as you have reasonable separation between (a) the code that does things like validation, constraint checks and eventually state modification at the model and view levels, and (b) whatever event-handling or other code starts the process. In this case, it’s that event-handling or other trigger code that is the part where bugs tend to be either obvious or outside of your immediate control, and you can have unit test suites for everything below. You can also use tools like Selenium to simulate those initial interactions and test in a more end-to-end fashion in real browsers.