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:

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:

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:

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:

  1. 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.
  2. 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": {}
}

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:

  1. utam CLI with the -c or -p flags
  2. Default utam.config.js configuration file
  3. 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

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

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

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

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

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

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 extcommonjsmodule
*.jsCJSESM
*.cjsCJS ¹
*.mjsESM

¹ Only generated if skipCommonJs is set to false

Note: CJS stands for CommonJS and ESM for ES Modules

skipCommonJs

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 to module

alias

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:

  1. the first part represents the package name (required)
  2. the path to the page object (optional)
  3. 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:

  1. Inline in the compiler configuration file
  2. 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

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

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

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:

injection-js

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.

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"
        }
    ]
}