Guide for JavaScript Developers
This document explains how to configure the UTAM compiler, generate UTAM JavaScript page objects and use them in a JavaScript or TypeScript UI test. Examples of tests and setup are available in the JavaScript recipes repo.
Compiler Setup
To configure the UTAM compiler, add a dependency for UTAM in your project. Optionally, create a JSON configuration file and declare the required options and the optional values for which the default values don't match your project structure.
Project Dependencies for JavaScript
The package.json
file configures the dependencies for a JavaScript project.
Add a development dependency for the UTAM compiler by running one of these commands:
yarn add utam --dev
npm install utam --save-dev
These commands are equivalent and add utam
to the devDependencies
property in package.json
, where x.y.z
represents the installed version and is the latest version by default.
{
"devDependencies": {
"utam": "x.y.z"
}
}
For a working example, see the utam-js-recipes repo.
The wdio-utam-service
dependency includes an adapter between UTAM page objects and the browser API that WebdriverIO provides. The dependency includes a transitive dependency on the UTAM compiler. If the project needs the UTAM compiler only, use the "utam"
dependency instead.
Add a development dependency for wdio-utam-service
by running one of these commands:
yarn add wdio-utam-service --dev
npm install wdio-utam-service --save-dev
Running one of these commands updates devDependencies
to look like this:
{
"devDependencies": {
"wdio-utam-service": "x.y.z"
}
}
Set Up Compile Step in package.json
This package.json
file has a script that runs the utam
compile CLI command. The utam
CLI is installed locally in your project because you added it to the devDependencies
in package.json
.
{
"scripts": {
"build": "yarn build:utam && yarn build:ts",
"build:ts": "tsc -b tsconfig.json",
"build:utam": "utam -c utam.config.json"
}
}
In this example, the compiler configuration file is
utam.config.json
.
The build
script name is arbitrary. You could use another name, such as compile
. Use the script name with yarn
or npm
to run the script. For example:
yarn build
or
npm run build
The "build:ts"
option is needed only if you're using TypeScript.
Compiler Configuration File
The compiler configuration file enables you to customize the options supported by the JavaScript compiler. We recommend utam.config.json
as the name of the compiler configuration file. The file can have any name, as long as the name matches the value of the -c
flag used with the utam
CLI.
After you've added utam
as a dependency, run the compiler using one of these approaches:
- Set up a script in
package.json
. If the script is calledbuild
, runyarn build
ornpm run build
. - Invoke the
utam
CLI. Use yarn:yarn utam -c utam.config.json
or npx:npx utam -c utam.config.json
. The-c
flag points the compiler at theutam.config.json
configuration file.
Some developers prefer to use a utam.config.js
configuration file because it's easier to add comments to a JavaScript file compared to a JSON file. The compiler automatically looks for a utam.config.js
configuration file at the project root. If you use utam.config.js
, you don't need to use the -c
flag.
Configure the JavaScript Compiler
To configure the JavaScript compiler, you have two options:
- Use the default convention, which is the easiest way to configure the compiler. It implies that your project structure adheres with some built-in assumptions; for example, where to find declarative page objects, and where to output compiled page objects. That's the recommended approach when starting a new project because you don't have to tweak the compiler configuration.
- Explicitly declare compiler options to fit your project structure. Consider this option when you integrate UTAM with an existing project or when the default convention doesn't align with your project structure. It gives you more control at the expense of additional configuration time.
JavaScript Compiler Default Convention
You don't have to explicitly declare any options for the JavaScript compiler because all options have default values. If your project
structure follows the default convention, the compiler works out of the box. In that case, set the utam.config.json
compiler
configuration file to be an empty object, {}
.
Here's an overview of all configuration options supported by the JavaScript compiler with their associated default values:
{
"pageObjectsRootDir": "./", // config file directory name
"pageObjectsFileMask": ["**/__utam__/**/*.utam.json"],
"extensionsFileMask": ["**/__utam__/**/*.utam.js"],
"pageObjectsOutputDir": "pageObjects",
"extensionsOutputDir": "utils",
"moduleTarget": "commonjs",
"skipCommonJs": false,
"alias": {},
"version": undefined,
"copyright": undefined,
"module": undefined,
"profiles": undefined
}
Let's look at an example project that follows the default convention.
<packageRoot>
├── package.json
├── utam.config.json
└── src/
└── modules/
└── component1/
└── __utam__/
└── component1.utam.json
├── component2/
└── __utam__/
├── component2.utam.json
└── extension.utam.js
..
└── componentN/
└── __utam__/
└── componentN.utam.json
This sample project structure follows the default convention. The utam.config.json
compiler configuration file is
at the package root level and is a sibling to package.json
. The declarative page objects are next to their associated components in __utam__
subfolders.
The resulting project structure after compiling is:
<packageRoot>
├── package.json
├── utam.config.json
├── pageObjects/
├── component1.d.ts
├── component1.js
├── component1.mjs
..
├── componentN.d.ts
├── componentN.js
└── componentN.mjs
├── src/
└── modules/
├── component1/
└── __utam__/
└── component1.utam.json
├── component2/
└── __utam__/
├── component2.utam.json
└── extension.utam.js
..
└── componentN/
└── __utam__/
└── componentN.utam.json
└── utils/
├── extension.js
└── extension.mjs
By following the default convention, the compiler finds the declarative page objects and extensions and
generates their respective implementations in pageObjects
and utils
folders at the package root level.
Explicitly Declare Compiler Options
Explicitly declare compiler options when the default convention doesn't align with your project structure.
There are multiple approaches to configure compiler options. We offer these different approaches to allow for different project structures and different developer preferences.
Use a compiler configuration file
You can specify options in the compiler configuration file.
Use the -c
flag of the utam
CLI to point at the configuration file. In this example, we use utam.config.json
.
utam -c utam.config.json
Use the -p
flag of the utam
CLI to point the compiler at multiple configuration files. This approach is useful if you have a repo where you need different compiler configuration options in different subprojects. In most scenarios, the -c
flag suffices and you don't need to use the -p
flag.
utam -p 'path/to/project1/utam.config.json' 'path/to/project2/utam.config.json'
Note: Pass a list of space separated utam compiler configuration file paths
Use a configuration object in package.json
Alternatively, you can specify options in a configuration object for the utam
key in package.json
. For example:
{
"devDependencies": {
"utam": "x.y.z"
},
"utam": {
"pageObjectsOutputDir": "pageObjectsJS",
"extensionsOutputDir": "pageObjects/utils"
}
}
As you can see, we offer multiple approaches to configure compiler options. With great flexibility comes the risk of over complicating your configuration. We recommend that you stick to one approach for each project for the sake of simplicity.
The compiler looks for configuration options in this order:
utam
CLI with the-c
or-p
flags- Default
utam.config.js
configuration file - Configuration object in
package.json
JavaScript Compiler Options Reference
This section lists all the options supported by the JavaScript compiler. For each option, we describe its JSON type, its default value, and what it does.
pageObjectsRootDir
- Type:
string
- Default:
path.dirname(configFilePath)
The relative path from the configuration file's directory that represents the root directory path. The compiler looks recursively in the root directory for declarative JSON page objects and extensions. It's also the directory in which the compiler generates compiled page objects and extensions.
Defaults to the configuration file's directory if not specified. For instance, if your utam.config.json
configuration file is
at the package root (the same level as the package.json
file), the page object root directory defaults to the package root directory.
pageObjectsFileMask
- Type:
string[]
- Default:
["**/__utam__/**/*.utam.json"]
The file mask pattern used by the compiler to find input JSON page objects. It tells the compiler
where to find the source JSON page objects relative to pageObjectsRootDir
. Declare the file mask as a list of glob
patterns.
For example, if we want to search for input files inside the src
directory only, set the pageObjectsFileMask
as follows:
{
"pageObjectsFileMask": ["src/**/__utam__/**/*.utam.json"]
}
extensionsFileMask
- Type:
string[]
- Default:
["**/__utam__/**/*.utam.js"]
The file mask pattern used by the compiler to find input extensions (JavaScript ESM extensions). It works exactly as the pageObjectsFileMask
option but for extensions rather than declarative page objects.
pageObjectsOutputDir
- Type:
string
- Default:
"pageObjects"
The relative path from the root directory to the target directory for generated page objects. That's where the compiler generates compiled JavaScript modules from the JSON page objects.
extensionsOutputDir
- Type:
string
- Default:
"utils"
The relative path from the root directory to the target directory for generated extensions. That's where the compiler copies and transpiles JavaScript extensions.
moduleTarget
- Type:
"module" | "commonjs"
- Default:
"commonjs"
The module system used for compiled page objects and extensions whose file extension is .js
. Configure this option to match
your package's module system. If your package uses ES Modules, set this option to "module"
. If your package uses CommonJS,
set this option to "commonjs"
or use the default value.
This table illustrates the module format used for a generated file extension depending on the moduleTarget
value.
Page Objects file ext | commonjs | module |
---|---|---|
*.js | CJS | ESM |
*.cjs | CJS ¹ | |
*.mjs | ESM |
¹ Only generated if skipCommonJs
is set to false
Note: CJS stands for CommonJS and ESM for ES Modules
skipCommonJs
- Type:
boolean
- Default:
false
Toggle page objects and extensions transpilation from ES Modules to CommonJS. If set to false
or missing,
the compiler transpiles ESM Page Objects and extensions to CJS. If set to true
, the compiler doesn't
transpile ESM page objects and extensions and only generates ES Modules.
Note: this option is only useful when
moduleTarget
is set tomodule
alias
- Type:
string | Record<string, string>
- Default:
{}
A collection of matching patterns and replacement values used to map type
values to import paths for custom elements and extensions. An alias provides flexibility by enabling you to match a type
value and replace it in the generated import path.
When you declare a custom element or extension in a declarative page object, the type
property represents the path
used by the platform module resolver. By default, the type
property value translates to the custom
element or extension module specifier.
Let's look at an example that illustrates how the compiler translates the type
property value into an import
path:
Let's declare a custom element in a JSON page object:
// filename: myPageObject.utam.json
{
"elements": [
{
"name": "myCustomElement",
"selector": {
"css": ".my-custom-element"
},
"type": "packageName/pageObjects/path/to/myCustomElement"
}
]
}
The import statement for that element in the generated page object is:
// filename: myPageObject.mjs
import _myCustomElement from 'packageName/pageObjects/path/to/myCustomElement';
That built-in behavior ensures that the type
property value has a specific semantic structure with three parts:
- the first part represents the package name (required)
- the path to the page object (optional)
- the page object name (required)
Sometimes, that coupling between the type
property value and the import path lacks flexibility.
For instance, you might want to use an internal import reference that's different from the package name published
on the public registry.
An alias decouples the 1-to-1 relationship between type
and import
paths. Let's look at how we can use aliases to
add a package scope in front of your custom element.
Define an alias in the compiler configuration file:
// filename: utam.config.json
{
"alias": {
"packageName/*": "@scope/packageName/*"
}
}
With this mapping, the compiler replaces all type
values that contain packageName
by @scope/packageName
.
The generated import statement is:
// filename: myPageObject.mjs
import _myCustomElement from '@scope/packageName/pageObjects/path/to/myCustomElement';
The *
character is replaced by everything it matches in the type
property value. Here, *
matches path/to/myCustomElement
so @scope/packageName/*
translates to @scope/packageName/path/to/myCustomElement
in the import path.
Note: the matching pattern is a simple string matching pattern that supports the
*
wildcard.
There are two ways to declare type aliases:
- Inline in the compiler configuration file
- Externally in another JSON file that is referenced in the compiler config file
To declare type aliases inline, set the alias
property value to be an object:
// filename: utam.config.json
{
"alias": {}
}
Declare all aliases as key-value pairs where the keys represent matching patterns and the values represent replacement values.
// filename: utam.config.json
{
"alias": {
// aliases are key-values of
// type Matching Pattern: import Replacement Value
"packageName/pageObjects/*": "@scope/packageName/pageObjects/*"
}
}
To declare type aliases externally, create a JSON file that defines the aliases:
// filename: utam-alias.config.json
{
"packageName/pageObjects/*": "@scope/packageName/pageObjects/*"
}
Then set the alias
property in the compiler configuration file to reference the aliases JSON config file:
// filename: utam.config.json
{
// assuming both config files are siblings
"alias": "./utam-alias.config.json"
}
The JavaScript compiler automatically resolves the alias configuration path and loads the aliases.
version
- Type:
string
- Default: the current date and time
Version is an optional string that is propagated as @version
in the generated page object JSDoc.
For example, if version is set:
{
"version": "Spring '22"
}
The generated JSDoc in the code is:
/**
* ...
* @version Spring '22
*/
export default class DescriptionClass extends _UtamBasePageObject {
If the version isn't set, the current date and time is used:
/**
* ...
* @version 2022-12-01 09:12:01
*/
export default class DescriptionClass extends _UtamBasePageObject {
copyright
- Type:
string[]
- Default: none
An optional array of strings that is added at the top of the generated page object. If the copyright is set:
{
"copyright": ["Copyright (c) 2022, salesforce.com, inc.", "All rights reserved."]
}
The generated JSDoc in the code is:
/**
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
*/
import { By as _By } from '@utam/core';
// ... class code
module
- Type:
String
- Default: none
The name of the module used to generate injections config. It's a mandatory configuration parameter if the UTAM compiler generates page objects for interfaces.
profiles
- Type: array of objects
- Default: none
Used in injections config. See interfaces.
interruptCompilerOnError
- Type:
boolean
- Default: true
By default, the compiler interrupts execution if a compilation error occurs. If this parameter is set to false
, the compiler continues to generate other page objects, combines all compilation errors into one report, and throws an error at the end. All the errors are added to the utam.errors.txt
file and printed to the console.
Test Setup
After you generate UTAM page objects, you can create a UI test that uses the page objects. Before your test can interact with the UI, UTAM needs to have access to a driver and to certain configuration parameters.
UTAM and WebdriverIO Integration
This package.json
file includes a dependency for WebdriverIO, which is an automation framework for JavaScript apps.
{
"devDependencies": {
"wdio-utam-service": "0.0.7"
}
}
wdio-utam-service
is an adapter between UTAM page objects and the browser API that WebdriverIO provides. The dependency includes a transitive dependency on the UTAM compiler.
WDIO config and utam service
The wdio.config.js
file configures WebdriverIO and requires wdio-utam-service
. It's set in the root folder of the project.
We describe only the relevant parts of the wdio configuration. For the full configuration, see the JavaScript recipes repository.
For the complete list of WebdriverIO configuration properties, see WebdriverIO's configuration page.
const { UtamWdioService } = require('wdio-utam-service');
...
exports.config = {
...
services: ['chromedriver', [UtamWdioService, {}]]
...
}
A driver instance is created by WebDriverIO. UtamWdioService
wraps the driver instance and uses the configured timeouts. Now, you can create an instance of a UTAM page object in a test.
Set Timeouts for JavaScript in WebDriverIO
The wdio.config.js
file also configures the timeouts and should be set depending on the speed of your testing environment.
// This service creates an instance of UtamLoader from a WebDriverIO driver
const { UtamWdioService } = require('wdio-utam-service');
// ...
exports.config = {
// ...
// sets explicit timeout to 5 seconds
waitforTimeout: 1000 * 5,
// optional parameter that sets the polling interval for explicit waits
waitforInterval: 200,
// ....
services: [
// WebDriverIO doesn't allow setting an implicit timeout through config
// so we set it via UtamWdioService parameters
[UtamWdioService, { implicitTimeoutMsec: 0 }]
]
// ...
};
waitforTimeout
property sets the default timeout for all waitFor*
basic actions and for explicit waits. This example sets the explicit wait timeout to 5 seconds.
The implicit wait timeout is set via implicitTimeout
argument of the UTAM WDIO service and we recommend using 0 (which is also the default).
Set up injection config for interfaces
If your UI tests use UTAM interfaces, you must set up injection configs.
Find injection configs in consumed modules
The injection config is generated with page objects. It can be:
- in the root folder of your local generated page objects
- or a file from a dependency package
Add injection configs to UtamLoader
Add injectionConfigs
to WebDriverIO config as a UtamWdioService parameter:
{
// ...
services: [
'chromedriver',
[
UtamWdioService,
{
implicitTimeout: 0,
injectionConfigs: [
// load a dependency config from a dependency
'vanilla-js-components/profile.config.json',
// load a dependency config local to the project
path.join(__dirname, '../path/to/anotherProfile.config.json')
]
}
]
],
// ...
}
Each string in the injectionConfigs
array should point to the actual JSON injection configuration file generated with page objects.
Set active profile
Setting the active profile affects which implementing class is picked. A common example is mobile tests, which must set the current platform in UtamWdioService:
// utam variable is instance of UtamLoader
utam.setProfile('platform', getMobilePlatformType(browser));
Mobile setup
To execute a test using a UTAM page object against a local simulator or emulator, the UTAM framework needs to know your local environment through the wdio configuration file.
For iOS:
exports.config = {
...baseMobileConfig,
capabilities: [
{
...
'appium:deviceName': 'iPhone 12',
'appium:app': '<path to iOS test app>',
'appium:platformVersion': '15.2',
},
]
};
For Android:
exports.config = {
...baseMobileConfig,
capabilities: [
{
...
'appium:deviceName': 'emulator-5554',
'appium:app': '<path to Android test app>',
'appium:appActivity': 'com.salesforce.chatter.Chatter',
'appium:appPackage': 'com.salesforce.chatter',
},
]
};
Start Appium server
Start an Appium server locally before executing any test by running appium --port 4444
.
Write UI Tests
Before you start to write test code, clarify the use case for your test. Identify the DOM elements that your test needs to exercise and ensure that those elements are accessible in your page objects. Start small and iterate when you're creating your first test and page objects.
The Write a Test tutorial gives you a good introduction to test code for a Hello World example. The UI enables you to see the DOM, JSON page object, generated JavaScript code, test code, and a DOM tree viewer. The sidebar walks you through the moving parts.
Let's look at the test code in the compose method tutorial.
// Import a root page object
import LoginFormRoot from 'tutorial/loginForm';
runPlaygroundTest(async () => {
// Load the page object
const loginFormRoot = await utam.load(LoginFormRoot);
const usernameElement = await loginFormRoot.getUsername();
const passwordElement = await loginFormRoot.getPassword();
await loginFormRoot.login('[email protected]', 'azerty123');
assert.strictEqual(await usernameElement.getValue(), '[email protected]');
assert.strictEqual(await passwordElement.getValue(), 'azerty123');
});
The
runPlaygroundTest()
method is specific to the tutorial environment. For a more generic test, see Sample Repo Using WebDriverIO.
Here are the basic steps you perform in test code.
Import the root page object
Import a root page object using JavaScript module syntax. You can use any name to refer to the default export from the module. The tutorial code uses LoginFormRoot
.
import LoginFormRoot from 'tutorial/loginForm';
Note: To load a page object directly from a UI test, the page object must declare
"root" : true
in its JSON page object file.
Load a page object
This line loads the root page object.
const loginFormRoot = await utam.load(LoginFormRoot);
The utam.load()
method guarantees that by the time you load, the root element is available and present. If the page object can't be loaded, you get an error. UTAM always tries to fail as fast as possible if there's an error. The fast failure makes it easier to locate the line of code that's failing when you debug a test.
Call page object methods
The page object exposes the getUsername()
, getPassword()
, and login()
methods so you can call them in your test.
It's a best practice to use a compose method instead of a public element in a page object whenever possible. A compose method provides better encapsulation of element structure and gives you better flexibility for page-object maintenance.
const usernameElement = await loginFormRoot.getUsername();
const passwordElement = await loginFormRoot.getPassword();
await loginFormRoot.login('[email protected]', 'azerty123');
The getPassword()
method returns an element on the page. You can then call a basic action on the element. The getValue()
basic action returns the value of an input
element's value
property. For example:
await passwordElement.getValue();
Use assertions
Use assertions to ensure that your test returns expected results. Use any assertion library of choice, such as Node.js assertions. For example:
assert.strictEqual(await passwordElement.getValue(), 'azerty123');
Run tests
The process to run tests depends on your test automation framework and the programming language that you use. For an example, see Sample Repo Using WebDriverIO.
We looked at the basic steps in a test. Now, let's see some other common actions in a test.
Use Developer Tools and console.log()
If you're playing around with tutorial code, you can add a debugger;
statement to invoke the Developer Tools debugger and step through your code. Call console.log()
to output a message to the browser console and to the tutorial console.
Wait until a condition is met
Write more durable tests by waiting for certain actions to complete and yield until the conditions are met. Use an explicit wait to wait for a condition to be ready for the test to proceed.
Use Jasmine to structure tests
Jasmine is a behavior-driven development framework for testing JavaScript code. This repo uses a WebdriverIO Jasmine plugin that's an adapter for the Jasmine testing framework.
When you look at the test code in utam-js-recipes repo, you see some standard Jasmine methods.
-
describe()
creates a group of specs, also known as a suite. -
it()
defines a single spec. A spec should contain one or more expectations that test the state of the code.
We don't have documentation yet for integrating with other test frameworks.
Code completion (IntelliSense) for UTAM JSON files
The JSON schema for UTAM page objects is available at Schema Store.
Most source code editors or IDEs support code completion (IntelliSense) for .utam.json
file extensions either automatically or through configuration.
Example configuration for Visual Studio Code
To configure IntelliSense for UTAM JSON files, add the following snippet to .vscode/settings.json
.
{
"json.schemas": [
{
"fileMatch": ["*.utam.json"],
"url": "https://json.schemastore.org/utam-page-object.json"
}
]
}