Appearance
Reusing and Composing Tests
As your test suite grows, it’s common to repeat the same flows across many tests — logging in, setting up test data, navigating to a specific page, or performing shared cleanup steps.
UI-licious encourages you to reuse and compose tests instead of duplicating these steps. This keeps tests shorter, easier to maintain, and less error-prone when flows change.
Composing tests with TEST.run
UI-licious provides TEST.run() to execute another test file from within the current test.
js
TEST.run("flows/login")This allows you to:
- extract shared flows into dedicated files
- compose tests from smaller, focused scripts
- avoid duplicating setup logic across tests
A common use case is reusing authentication flows.
Instead of repeating login steps in every test:
js
I.fill("Email", "[email protected]")
I.fill("Password", "secret")
I.click("Log In")Extract them into a shared flow:
js
// file: flows/login.js
I.fill("Email", "[email protected]")
I.fill("Password", "secret")
I.click("Log In")Then, reuse it:
js
TEST.run("flows/login")
I.see("Dashboard")Important: Shared JavaScript Context
All test files executed via TEST.run() share the same JavaScript context and scope.
This means that:
- variables declared in one test file are accessible in subsequently run test files
- changes to variable values propagate to subsequent test files
Because of this, re-declaring the same variable name using let or const across test files may result in runtime errors.
If you need isolation between test files, wrap the test logic in a self-invoking function:
js
(function () {
var data = TEST.loadDataFromJson("data/admin-user.json")
I.fill("Username", data.username)
I.fill("Password", data.password)
I.click("Log In")
})()This confines variables to the function scope TEST.run().
Shared setup and utility functions
In addition to shared flows, you may also want to reuse setup logic or helper functions across tests.
For setup or utility code that should only run once per test run, use TEST.runOnce() instead of TEST.run(). Subsequent calls to TEST.runOnce() with the same file path are ignored.
For example, a utility file for date formatting:
js
// file: utils/date_formatter
function formatDate_ddmmyyyy(date) {
if (!(date instanceof Date) || isNaN(date)) {
throw new Error("Invalid Date object")
}
const day = String(date.getDate()).padStart(2, "0")
const month = String(date.getMonth() + 1).padStart(2, "0") // months are 0-based
const year = date.getFullYear()
return `${day}/${month}/${year}`
}You can load the utility file and then use the functions in a test:
js
// load utilities
TEST.runOnce("utils/date_formatter")
// begin test case
var now = new Date()
I.fill("Check-in date", formatDate_ddmmyyyy(now))Page Object Models
For larger or more complex flows, you can structure reusable logic around a feature using Page Object Models, implemented as plain JavaScript objects.
For example, a page object for a login form:
js
// file: models/login_form
var LOGIN_FORM = {
fill(username, password){
I.fill("Username", username)
I.fill("Password", password)
},
clear(){
I.clear("Username")
I.clear("Password")
},
submit(){
I.click("Log In")
},
hasError(message){
// check error css is applied
I.see("#login-form.has-error")
// validate message is shown
if(message){
I.see(message)
}
},
}Load the page object using TEST.run or TEST.runOnce:
js
TEST.run("models/login_form")Then use it directly in your test:
js
LOGIN_FORM.fill("john", "supersecretpassword")
LOGIN_FORM.submit()
LOGIN_FORM.hasError("Invalid username or password.")Page Object Models help keep tests readable while avoiding duplication, without introducing additional frameworks or abstractions.