ufo-architecture.md
August 26, 2024 · View on GitHub
Write lighthouse user flows with UFO's 🛸
When writing tests for multiple interactions or pages, or even for different platforms you are forced to organize your code propperly. Otherwise, the amount of low-level code becomes a night mare to maintain...
This is the reason we introduced UFO's!
Organize cluttery lighthouse user flows code 👽 in developer friendly shells 🛸
It pays off to organize your code in a reusable way and abstract certain different layers of logic:
- UI selectors (the actual DOM selectors which may change often even if the UI stays visually the same)
- Atomic UI interactions and performance relevant logic
- User interaction flow including all steps and measures
UFO's (user-flow objects) are a very similar to the concept of PageObjects where you separate the API's for accessing the actual UI form the application related code. The difference however is that the higher layer focuses on performance related interactions.
Here we can introcude terms related to lighthouse user-flow. For example every object could have a CLP element, or we detect when a UI-Facade got interactive.
Let's first think about the folder structure and how we organize the different pieces:
Folder Structure
📦my-app
┣ 📂src
┣ 📂dist
┣ ┗ 📂my-app-user-flows
┣ ...
┗ 📂my-app-user-flows
┣ 📄.user-flowrc.json
┣ 📂fixtures
┣ ┗ 📜list-page.ufo.mts
┣ 📂ufo
┣ ┣ 📜sidebar.ufo.mts
┣ ┣ 📜list.ufo.mts
┣ ┗ 📜list-page.ufo.mts
┗ 📂user-flows
┗ 📜my-user-flow.uf.mts
Setup
Move .user-flowrc.json into my-app-user-flows and change the configuration to:
./my-app-user-flows/.user-flowrc.json
{
"collect": {
"url": "http:localhost:4200/",
"ufPath": "./my-app-user-flows/user-flows"
},
"persist": {
"outPath": "./dist/my-app-user-flows",
"format": ["html"]
}
}
Fixtures
Fixtures maintain all your UI related selector login. Mostly this are strings but also getter functions are possible to be organized here.
ℹ Tip: Avoid selectors that are connected to layout and styles e.g.
#main-side-bar div.header-wrapper .hamburger-btn. Instead introduce a data attribute only related to user flow tests e.g.*[data-uf="sidebar--main-btn"]Usage:<button data-uf="sidebar--main-bt">...</button>
const sidebarBtnSelector = '*[data-uf="sidebar--main-btn"]';
const SidebarListLinkSelector = '*[data-uf="sidebar--list-link"]';
const movieListImgSelector = (idx: number): string => `*[data-uf="movie-list--img-${idx}"]`;
UFO's 🛸 - user flow objects
User flow objects maintain UI elements relevant for the interactions and to determine when certain interactions like navigation or rendering are done. This objects are composeable, meaning they maintain units that can be reused within each other or grouped to bigger units.
In our case we have the sidebar, and the target page displaying the list. We extend from the Ufo class to get the page object set up for us and ensure the sidebar creation got passed the page object in the actual user-flow.
ℹ Tip: To ensure the page object is available in an UFO you can extend from
Ufoprovided in@push-based/user-flow.
./ufo/sidebar.ufo.mts
export class Sidebar extends Ufo {
btnSelector = fixtures.sidebarBtnSelector;
listLinkSelector = fixtures.sidebarListLink;
async clickSidebarBtn() {
await this.page.waitForSelector(this.btnSelector);
await this.page.click(this.btnSelector);
}
async navigateToList() {
await this.page.waitForSelector(this.listLinkSelector);
await this.page.click(this.listLinkSelector);
}
}
./ufo/list.ufo.mts
export class List extends Ufo {
itemSelector = fixtures.movieListImgSelector;
async clickItem(idx: number = 0) {
const item = this.itemSelector(idx);
await this.page.waitForSelector(item);
await this.page.click(item);
}
async awaitLCP() {
await this.page.waitForSelector(this.itemSelector(0));
}
}
./ufo/list-page.ufo.mts
// ensure page is present 👇
export class ListPage extends Ufo {
list = new List(this.page);
async awaitLCP() {
await this.list.awaitLCP();
}
}
User Flows
./user-flows/my-user-flow.uf.mts
import {
UserFlowOptions,
UserFlowInteractionsFn,
UserFlowContext,
UserFlowProvider
} from '@push-based/user-flow';
const flowOptions: UserFlowOptions = {
name: 'Category to Detail Navigation - Cold',
};
const interactions: UserFlowInteractionsFn = async (ctx: UserFlowContext): Promise<any> => {
const { page, flow, baseUrl } = ctx;
const url = `${baseUrl}/list`;
// 👇 Setup UFO's
const sidebar = new SideBar({page});
const listPage = new ListPage({page});
await flow.startTimespan({
name: 'Navigate to list page',
});
// 👇 Interaction logic
await sidebar.awaitLCP();
await listPage.awaitLCP();
await flow.endTimespan();
};
export default {
flowOptions: { name: "Order Coffee" },
interactions,
} satisfies UserFlowProvider;
Usage
To run the user flows use the -p option to point to the new config file
npx @push-based/user-flow -p=./my-app-user-flows/.user-flowrc.json
made with ❤ by push-based.io