├── .forceignore
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── babel.config.js
├── config
└── project-scratch-def.json
├── eslint.config.js
├── force-app
├── main
│ └── default
│ │ └── lwc
│ │ └── helloWorld
│ │ ├── __tests__
│ │ └── helloWorld.test.js
│ │ ├── helloWorld.html
│ │ ├── helloWorld.js
│ │ └── helloWorld.js-meta.xml
└── test
│ └── utam
│ ├── utam-examples.spec.js
│ └── utam-helper.js
├── jest-sa11y-setup.js
├── jest.config.js
├── package-lock.json
├── package.json
├── scripts
├── apex
│ └── hello.apex
├── generate-login-url.js
└── soql
│ └── account.soql
├── sfdx-project.json
├── utam.config.js
└── wdio.conf.js
/.forceignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
3 | #
4 |
5 | package.xml
6 |
7 | # LWC configuration files
8 | **/jsconfig.json
9 | **/.eslintrc.json
10 |
11 | # LWC Jest
12 | **/__tests__/**
13 | **/tsconfig.json
14 |
15 | **/*.ts
16 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Following owners will be requested for review when someone opens a pull request.
2 | * @svierk
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: 'npm' # See documentation for possible values
9 | directory: '/' # Location of package manifests
10 | schedule:
11 | interval: 'monthly'
12 | groups:
13 | dependencies:
14 | patterns:
15 | - '*'
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: validation
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | validation:
12 | name: Code Quality
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@main
17 | with:
18 | fetch-depth: 0
19 | - name: Select Node Version
20 | uses: svierk/get-node-version@main
21 | - name: Install Dependencies
22 | run: npm ci
23 | - name: Check Prettier
24 | run: npm run prettier
25 | - name: Check ESLint
26 | run: npm run lint:sonar
27 | - name: LWC Unit Tests
28 | run: npm run test:unit:coverage
29 |
30 | tests:
31 | name: E2E UI Tests
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@main
36 | with:
37 | fetch-depth: 0
38 | - name: Select Node Version
39 | uses: svierk/get-node-version@main
40 | - name: Install Dependencies
41 | run: npm ci
42 | - name: Install SF CLI
43 | uses: svierk/sfdx-cli-setup@main
44 | - name: Salesforce Org Login
45 | uses: svierk/sfdx-login@main
46 | with:
47 | client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
48 | jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
49 | username: ${{ secrets.SFDX_USERNAME }}
50 | - name: Compile UTAM Page Objects
51 | run: npm run test:ui:compile
52 | - name: Prepare Login Details
53 | run: npm run test:ui:generate:login
54 | - name: UTAM E2E Tests
55 | run: npm run test:ui
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
3 | # For useful gitignore templates see: https://github.com/github/gitignore
4 |
5 | # Salesforce cache
6 | .sf/
7 | .sfdx/
8 | .localdevserver/
9 | deploy-options.json
10 |
11 | # LWC VSCode autocomplete
12 | **/lwc/jsconfig.json
13 |
14 | # LWC Jest coverage and test reports
15 | coverage/
16 | tests/
17 |
18 | # Logs
19 | logs
20 | *.log
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # Dependency directories
26 | node_modules/
27 |
28 | # Eslint cache
29 | .eslintcache
30 | eslint-report.json
31 |
32 | # MacOS system files
33 | .DS_Store
34 |
35 | # Windows system files
36 | Thumbs.db
37 | ehthumbs.db
38 | [Dd]esktop.ini
39 | $RECYCLE.BIN/
40 |
41 | # Local environment variables
42 | .env
43 |
44 | # UTAM
45 | utam-lint.sarif
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npm run precommit
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running prettier
2 | # More information: https://prettier.io/docs/en/ignore.html
3 | #
4 |
5 | **/staticresources/**
6 | .localdevserver
7 | .sfdx
8 | .vscode
9 |
10 | coverage/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@prettier/plugin-xml", "prettier-plugin-apex"],
3 | "trailingComma": "none",
4 | "overrides": [
5 | {
6 | "files": "**/lwc/**/*.html",
7 | "options": { "parser": "lwc" }
8 | },
9 | {
10 | "files": "*.{cmp,page,component}",
11 | "options": { "parser": "html" }
12 | }
13 | ],
14 | "singleQuote": true,
15 | "printWidth": 120
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "salesforce.salesforcedx-vscode",
4 | "redhat.vscode-xml",
5 | "dbaeumer.vscode-eslint",
6 | "esbenp.prettier-vscode",
7 | "financialforce.lana",
8 | "eamodio.gitlens",
9 | "chuckjonas.apex-pmd"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch Apex Replay Debugger",
9 | "type": "apex-replay",
10 | "request": "launch",
11 | "logFile": "${command:AskForLogFileName}",
12 | "stopOnEntry": true,
13 | "trace": true
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "**/node_modules": true,
4 | "**/bower_components": true,
5 | "**/.sfdx": true
6 | },
7 | "files.eol": "\n",
8 | "editor.defaultFormatter": "esbenp.prettier-vscode",
9 | "editor.formatOnSave": true,
10 | "editor.rulers": [120],
11 | "editor.bracketPairColorization.enabled": true,
12 | "files.associations": {
13 | "*.json": "jsonc",
14 | "package.json": "json",
15 | "package-lock.json": "json"
16 | },
17 | "[javascript]": {
18 | "editor.codeActionsOnSave": {
19 | "source.organizeImports": "explicit"
20 | }
21 | },
22 | "eslint.options": {
23 | "extensions": [".js", ".html"]
24 | },
25 | "eslint.validate": ["javascript", "html"],
26 | "salesforcedx-vscode-core.retrieve-test-code-coverage": true
27 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Sebastiano Schwarz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🧪 Salesforce UTAM E2E Testing
2 |
3 | 
4 |
5 | ## About the project
6 |
7 | This repository provides a template for setting up E2E UI test automation with [UTAM](https://utam.dev/) (UI Test Automation Model) in Salesforce DX projects.
8 | In addition to the basic test setup configuration, a number of sample test cases are included to help you get started with UTAM. The configuration shown here was set up based on the contents of the following two repositories:
9 |
10 | - [UTAM JavaScript Recipes](https://github.com/salesforce/utam-js-recipes) | Various examples of how to test the Salesforce UI with UTAM
11 | - [Salesforce E-Bikes App](https://github.com/trailheadapps/ebikes-lwc) | LWC sample application with UTAM UI tests
12 |
13 | ## Prerequisites
14 |
15 | To use this template, the [Node](https://nodejs.org/en/) version specified in the _package.json_ and the latest version of the [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli) should already be installed.
16 |
17 | ## Getting started
18 |
19 | Follow the steps below to get the template running and manually execute the sample tests already included. The sample tests should be generic enough to run in any Salesforce Org without specific configuration, such as a Trailhead Playground.
20 |
21 | 1. First clone the repository and open it with VS Code, install all the recommended extensions and run the following command to install all required dependencies:
22 |
23 | ```
24 | npm install
25 | ```
26 |
27 | 2. Next you need to authorize an org for which you want to run the tests. In VS Code this can be done by pressing **Command + Shift + P**, enter "sfdx", and select **SFDX: Authorize an Org**. Alternatively you can also run the following command from the command line:
28 |
29 | ```
30 | sf org login web
31 | ```
32 |
33 | 3. Compile all UTAM page objects:
34 |
35 | ```
36 | npm run test:ui:compile
37 | ```
38 |
39 | 4. Prepare the Salesforce login information:
40 |
41 | ```
42 | npm run test:ui:generate:login
43 | ```
44 |
45 | 5. Finally, execute all existing UI tests:
46 |
47 | ```
48 | npm run test:ui
49 | ```
50 |
51 | **Note:** By default, the tests are executed in headless mode with the given configuration, i.e. the browser does not open visibly but runs tests in the background. This configuration is primarily intended for automatic execution in CI/CD pipelines. To deactivate the headless mode for local development and testing purposes, please comment out the following line in _wdio.conf.js_:
52 |
53 | ```
54 | args: ['--headless']
55 | ```
56 |
57 | ## How to write your own tests
58 |
59 | Creating UTAM tests is not trivial and the setup may involve one or two hurdles. Fortunately, there is a handy [UTAM Chrome Browser Extension](https://utam.dev/tools/browser-extension) to help with writing the tests. This extension helps to identify the page objects of interest directly in the Salesforce org and generates the corresponding test code in the selected language:
60 |
61 |
62 |
63 | ## Automated test execution with GitHub Actions
64 |
65 | UTAM tests can also be executed automatically in headless mode within a pipeline. The following Medium article describes how to an automated UTAM test execution in detail:
66 |
67 | [Automate Salesforce E2E Testing using UTAM & GitHub Actions](https://medium.com/capgemini-salesforce-architects/automate-salesforce-e2e-testing-using-utam-github-actions-b11906fefc85)
68 |
69 | Below is an example with GitHub Actions, which is also used in this repository and can be found in the _.github_ directory:
70 |
71 | ```
72 | tests:
73 | name: E2E UI Tests
74 | runs-on: ubuntu-latest
75 | steps:
76 | - name: Checkout
77 | uses: actions/checkout@main
78 | with:
79 | fetch-depth: 0
80 | - name: Select Node Version
81 | uses: svierk/get-node-version@main
82 | - name: Install Dependencies
83 | run: npm ci
84 | - name: Install SF CLI
85 | uses: svierk/sfdx-cli-setup@main
86 | - name: Salesforce Org Login
87 | uses: svierk/sfdx-login@main
88 | with:
89 | client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
90 | jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
91 | username: ${{ secrets.SFDX_USERNAME }}
92 | - name: Compile UTAM Page Objects
93 | run: npm run test:ui:compile
94 | - name: Prepare Login Details
95 | run: npm run test:ui:generate:login
96 | - name: UTAM E2E Tests
97 | run: npm run test:ui
98 | ```
99 |
100 | ## Learn more about UTAM
101 |
102 | - [UTAM Website](https://utam.dev/) | Official UTAM Documentation
103 | - [Run End-to-End Tests with the UI Test Automation Model (UTAM)](https://developer.salesforce.com/blogs/2022/05/run-end-to-end-tests-with-the-ui-test-automation-model-utam) | Post in Salesforce Developers' Blog
104 | - [Run End-to-End Tests With UTAM](https://www.youtube.com/watch?v=rxZfsjIwWeU) | YouTube video that provides a 15min UTAM overview
105 | - [Getting Started with UTAM](https://www.youtube.com/watch?v=YMxeCJexgMY) | YouTube video about a 1h step by step guide for writing a UTAM test
106 | - [Streamline E2E Testing with UTAM: Salesforce’s UI Test Automation Model](https://medium.com/capgemini-salesforce-architects/streamline-e2e-testing-with-utam-salesforces-ui-test-automation-model-51c0effb1e67) | Medium Post
107 | - [Automate Salesforce E2E Testing using UTAM & GitHub Actions](https://medium.com/capgemini-salesforce-architects/automate-salesforce-e2e-testing-using-utam-github-actions-b11906fefc85) | Medium Post
108 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: '16'
8 | }
9 | }
10 | ]
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "svierk company",
3 | "edition": "Developer",
4 | "features": ["EnableSetPasswordInApi"],
5 | "settings": {
6 | "lightningExperienceSettings": {
7 | "enableS1DesktopEnabled": true
8 | },
9 | "mobileSettings": {
10 | "enableS1EncryptedStoragePref2": false
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { defineConfig } = require('eslint/config');
4 | const eslintJs = require('@eslint/js');
5 | const jestPlugin = require('eslint-plugin-jest');
6 | const salesforceLwcConfig = require('@salesforce/eslint-config-lwc/recommended');
7 | const i18n = require('@salesforce/eslint-config-lwc/i18n');
8 | const globals = require('globals');
9 |
10 | module.exports = defineConfig([
11 | // LWC configuration for force-app/main/default/lwc
12 | {
13 | files: ['force-app/main/default/lwc/**/*.js'],
14 | extends: [salesforceLwcConfig, i18n],
15 | rules: {
16 | '@lwc/lwc/no-async-operation': 'off',
17 | '@lwc/lwc/consistent-component-name': 'error',
18 | '@lwc/lwc/no-deprecated': 'error',
19 | '@lwc/lwc/valid-api': 'error',
20 | '@lwc/lwc/no-document-query': 'error',
21 | 'no-console': 'error',
22 | 'spaced-comment': ['error', 'always'],
23 | 'no-var': 'error',
24 | 'prefer-const': 'error',
25 | 'prefer-spread': 'error',
26 | 'prefer-object-spread': 'error',
27 | 'prefer-template': 'error',
28 | camelcase: 'error',
29 | 'max-lines': ['error', 500],
30 | 'max-lines-per-function': ['error', 50],
31 | 'no-inline-comments': 'error',
32 | 'no-nested-ternary': 'error'
33 | }
34 | },
35 |
36 | // LWC configuration with override for LWC test files
37 | {
38 | files: ['force-app/main/default/lwc/**/*.test.js'],
39 | languageOptions: { globals: { ...globals.node } },
40 | extends: [salesforceLwcConfig],
41 | rules: {
42 | '@lwc/lwc/no-unexpected-wire-adapter-usages': 'off',
43 | '@locker/locker/distorted-element-shadow-root-getter': 'off',
44 | 'max-lines-per-function': 'off'
45 | }
46 | },
47 |
48 | // Jest mocks configuration
49 | {
50 | files: ['force-app/test/jest-mocks/**/*.js'],
51 | languageOptions: {
52 | sourceType: 'module',
53 | ecmaVersion: 'latest',
54 | globals: { ...globals.node, ...globals.es2021, ...jestPlugin.environments.globals.globals }
55 | },
56 | plugins: { eslintJs },
57 | extends: ['eslintJs/recommended']
58 | },
59 |
60 | // UTAM tests configuration
61 | {
62 | files: ['force-app/test/utam/**/*.js'],
63 | languageOptions: {
64 | sourceType: 'module',
65 | ecmaVersion: 'latest',
66 | globals: { ...globals.node, ...globals.es2021, ...globals.jasmine, utam: 'readonly', browser: 'readonly' }
67 | },
68 | plugins: { eslintJs },
69 | extends: ['eslintJs/recommended']
70 | }
71 | ]);
72 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/helloWorld/__tests__/helloWorld.test.js:
--------------------------------------------------------------------------------
1 | import HelloWorld from 'c/helloWorld';
2 | import { createElement } from 'lwc';
3 |
4 | describe('c-hello-world', () => {
5 | afterEach(() => {
6 | while (document.body.firstChild) {
7 | document.body.removeChild(document.body.firstChild);
8 | }
9 | });
10 |
11 | it('is accessible and displays greeting', async () => {
12 | // given
13 | const element = createElement('c-hello-world', {
14 | is: HelloWorld
15 | });
16 | element.name = 'World';
17 |
18 | // when
19 | document.body.appendChild(element);
20 |
21 | // then
22 | const div = element.shadowRoot.querySelector('div');
23 | expect(div.textContent).toBe('Hello, World!');
24 | await expect(element).toBeAccessible();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/helloWorld/helloWorld.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello, {name}!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/helloWorld/helloWorld.js:
--------------------------------------------------------------------------------
1 | import { api, LightningElement } from 'lwc';
2 |
3 | /**
4 | * An example LWC that adds a classic greeting to any page.
5 | * @alias HelloWorld
6 | * @extends LightningElement
7 | * @hideconstructor
8 | *
9 | * @example
10 | *
11 | */
12 | export default class HelloWorld extends LightningElement {
13 | /**
14 | * Enter the name of the person to greet.
15 | * @type {string}
16 | * @default 'World'
17 | */
18 | @api name = 'World';
19 | }
20 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/helloWorld/helloWorld.js-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 63.0
4 | true
5 | Hello World
6 | Add a classic greeting to any page.
7 |
8 | lightning__AppPage
9 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/force-app/test/utam/utam-examples.spec.js:
--------------------------------------------------------------------------------
1 | import ObjectHomeDesktop from 'salesforce-pageobjects/force/pageObjects/objectHome';
2 | import AppLauncherMenu from 'salesforce-pageobjects/global/pageObjects/appLauncherMenu';
3 | import RecordActionWrapper from 'salesforce-pageobjects/global/pageObjects/recordActionWrapper';
4 | import DesktopLayoutContainer from 'salesforce-pageobjects/navex/pageObjects/desktopLayoutContainer';
5 | import { logInSalesforce } from './utam-helper.js';
6 |
7 | describe('utam-examples', () => {
8 | beforeEach(async () => {
9 | await logInSalesforce();
10 | });
11 |
12 | it('navigate to service app', async () => {
13 | // navigate to the app launcher
14 | const container = await utam.load(DesktopLayoutContainer);
15 | const appNav = await container.getAppNav();
16 | const appLauncher = await (await appNav.getAppLauncherHeader()).getButton();
17 | await appLauncher.click();
18 |
19 | // search for the service app
20 | const menu = await utam.load(AppLauncherMenu);
21 | const search = await (await menu.getSearchBar()).getLwcInput();
22 | await search.setText('Service');
23 |
24 | // get all items and click first search result
25 | const items = await menu.getItems();
26 | await (await items[0].getRoot()).click();
27 |
28 | // get the name of the currently active app
29 | const appName = await (await appNav.getAppName()).getText();
30 |
31 | // assert that you have navigated to the correct app
32 | expect(appName).toEqual('Service');
33 | });
34 |
35 | it('navigate to accounts tab', async () => {
36 | // select the navigation bar of the current app
37 | const container = await utam.load(DesktopLayoutContainer);
38 | const appNav = await container.getAppNav();
39 | const appNavBar = await appNav.getAppNavBar();
40 |
41 | // select and click the accounts tab
42 | const tab = await appNavBar.getNavItem('Accounts');
43 | await tab.clickAndWaitForUrl('lightning/o/Account/list?filterName=__Recent');
44 |
45 | // select current list view
46 | const listView = await (await utam.load(ObjectHomeDesktop)).getListView();
47 | const listViewHeader = await listView.getHeader();
48 | const listViewName = await listViewHeader.getSelectedListViewName();
49 |
50 | // assert that you have selected the correct list view
51 | expect(listViewName).toEqual('Recently Viewed');
52 | });
53 |
54 | it('create an account', async () => {
55 | // navigate to the app launcher
56 | const container = await utam.load(DesktopLayoutContainer);
57 | const appNav = await container.getAppNav();
58 |
59 | // select the navigation bar of the current app
60 | const appNavBar = await appNav.getAppNavBar();
61 |
62 | // select and click the accounts tab
63 | const tab = await appNavBar.getNavItem('Accounts');
64 | await tab.clickAndWaitForUrl('lightning/o/Account/list?filterName=__Recent');
65 |
66 | // select current list view
67 | const listView = await (await utam.load(ObjectHomeDesktop)).getListView();
68 | const listViewHeader = await listView.getHeader();
69 |
70 | // click on new account action
71 | await (await listViewHeader.waitForAction('New')).click();
72 |
73 | // select account name input and enter name
74 | const modal = await utam.load(RecordActionWrapper);
75 | const recordForm = await modal.getRecordForm();
76 | const input = await (await (await recordForm.getRecordLayout()).getItem(1, 2, 1)).getTextInput();
77 | await input.setText('UTAM Test');
78 |
79 | // click modal save button
80 | await recordForm.clickFooterButton('Save');
81 | await modal.waitForAbsence();
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/force-app/test/utam/utam-helper.js:
--------------------------------------------------------------------------------
1 | const SESSION_TIMEOUT = 2 * 60 * 60 * 1000; // 2 hours by default
2 |
3 | /**
4 | * Checks environment variables, session timeout and logs into Salesforce
5 | * @returns {DocumentUtamElement} the UTAM DOM handle
6 | */
7 | export async function logInSalesforce() {
8 | // Check environment variables
9 | ['SALESFORCE_LOGIN_URL', 'SALESFORCE_LOGIN_TIME'].forEach((varName) => {
10 | if (!process.env[varName]) {
11 | throw new Error(`Missing ${varName} environment variable`);
12 | }
13 | });
14 | const { SALESFORCE_LOGIN_URL, SALESFORCE_LOGIN_TIME } = process.env;
15 |
16 | // Check for Salesforce session timeout
17 | if (new Date().getTime() - parseInt(SALESFORCE_LOGIN_TIME, 10) > SESSION_TIMEOUT) {
18 | throw new Error(`Salesforce session timed out. Re-authenticate before running tests.`);
19 | }
20 |
21 | // Navigate to login URL
22 | await browser.navigateTo(SALESFORCE_LOGIN_URL);
23 |
24 | // Wait for home page URL
25 | const domDocument = utam.getCurrentDocument();
26 | await domDocument.waitFor(async () => (await domDocument.getUrl()).endsWith('/home'));
27 | return domDocument;
28 | }
29 |
--------------------------------------------------------------------------------
/jest-sa11y-setup.js:
--------------------------------------------------------------------------------
1 | import { registerSa11yMatcher } from '@sa11y/jest';
2 |
3 | registerSa11yMatcher();
4 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
2 | const setupFilesAfterEnv = jestConfig.setupFilesAfterEnv || [];
3 | setupFilesAfterEnv.push('/jest-sa11y-setup.js');
4 |
5 | module.exports = {
6 | ...jestConfig,
7 | testRegex: '/__tests__/.*.test.js$',
8 | coverageReporters: ['clover', 'json', 'text', 'lcov', 'cobertura'],
9 | modulePathIgnorePatterns: ['/.localdevserver'],
10 | reporters: [
11 | 'default',
12 | [
13 | 'jest-junit',
14 | {
15 | outputDirectory: 'tests',
16 | outputName: 'test-results-lwc.xml'
17 | }
18 | ]
19 | ],
20 | setupFilesAfterEnv
21 | };
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "salesforce-utam-e2e-testing",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "SFDX template for using UTAM to establish Salesforce E2E UI test automation in your project",
6 | "author": {
7 | "name": "Sebastiano Schwarz",
8 | "url": "https://github.com/svierk"
9 | },
10 | "license": "MIT",
11 | "engines": {
12 | "node": "20.x.x",
13 | "npm": "10.x.x"
14 | },
15 | "scripts": {
16 | "lint": "eslint **/force-app/**/*.js",
17 | "lint:sonar": "eslint -f json -o eslint-report.json **/lwc/**/*.js",
18 | "test": "npm run test:unit",
19 | "test:unit": "sfdx-lwc-jest --skipApiVersionCheck",
20 | "test:unit:watch": "sfdx-lwc-jest --watch --skipApiVersionCheck",
21 | "test:unit:debug": "sfdx-lwc-jest --debug --skipApiVersionCheck",
22 | "test:unit:coverage": "sfdx-lwc-jest --coverage --skipApiVersionCheck",
23 | "test:ui": "wdio",
24 | "test:ui:compile": "utam -c utam.config.js",
25 | "test:ui:generate:login": "node scripts/generate-login-url.js",
26 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\" --check",
27 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"",
28 | "precommit": "lint-staged",
29 | "prepare": "husky"
30 | },
31 | "devDependencies": {
32 | "@babel/cli": "^7.27.1",
33 | "@babel/core": "^7.27.1",
34 | "@babel/eslint-parser": "^7.27.1",
35 | "@babel/preset-env": "^7.27.1",
36 | "@babel/register": "^7.27.1",
37 | "@lwc/eslint-plugin-lwc": "^3.0.0",
38 | "@prettier/plugin-xml": "^3.4.1",
39 | "@sa11y/jest": "^7.0.1",
40 | "@salesforce/eslint-config-lwc": "^4.0.0",
41 | "@salesforce/eslint-plugin-lightning": "^2.0.0",
42 | "@salesforce/sfdx-lwc-jest": "^7.0.1",
43 | "@wdio/cli": "^8.39.1",
44 | "@wdio/jasmine-framework": "^8.39.1",
45 | "@wdio/local-runner": "^8.39.1",
46 | "@wdio/spec-reporter": "^8.39.0",
47 | "chromedriver": "^135.0.0",
48 | "dotenv": "^16.5.0",
49 | "eslint": "^9.26.0",
50 | "eslint-plugin-compat": "^6.0.2",
51 | "eslint-plugin-import": "^2.31.0",
52 | "eslint-plugin-jasmine": "^4.2.2",
53 | "eslint-plugin-jest": "^28.11.0",
54 | "eslint-plugin-wdio": "^8.37.0",
55 | "husky": "^9.1.7",
56 | "jest-junit": "^16.0.0",
57 | "lint-staged": "^15.5.2",
58 | "prettier": "^3.5.3",
59 | "prettier-plugin-apex": "^2.2.6",
60 | "salesforce-pageobjects": "^7.0.1",
61 | "typescript": "~5.8.3",
62 | "utam": "^3.2.1",
63 | "wdio-chromedriver-service": "^8.1.1",
64 | "wdio-utam-service": "^3.2.1"
65 | },
66 | "lint-staged": {
67 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [
68 | "prettier --write --check"
69 | ],
70 | "**/lwc/**/*.{css,html,js}": [
71 | "eslint",
72 | "sfdx-lwc-jest --skipApiVersionCheck -- --passWithNoTests"
73 | ]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/scripts/apex/hello.apex:
--------------------------------------------------------------------------------
1 | // Use .apex files to store anonymous Apex.
2 | // You can execute anonymous Apex in VS Code by selecting the
3 | // apex text and running the command:
4 | // SFDX: Execute Anonymous Apex with Currently Selected Text
5 | // You can also execute the entire file by running the command:
6 | // SFDX: Execute Anonymous Apex with Editor Contents
7 |
8 | string tempvar = 'Enter_your_name_here';
9 | System.debug('Hello World!');
10 | System.debug('My name is ' + tempvar);
--------------------------------------------------------------------------------
/scripts/generate-login-url.js:
--------------------------------------------------------------------------------
1 | const util = require('util');
2 | const exec = util.promisify(require('child_process').exec);
3 | const { writeFileSync } = require('fs');
4 | const { join } = require('path');
5 | const DOTENV_FILEPATH = join(__dirname, '../.env');
6 |
7 | /**
8 | * Get the scratch org login url from a child CLI process and parse it
9 | * @returns {string} the scratch org url fetched from the getUrlCmd
10 | */
11 | async function getScratchOrgLoginUrl() {
12 | try {
13 | const getUrlCmd = 'sf org open -p /lightning -r --json';
14 | console.log('Executing the following command: ', getUrlCmd);
15 | const { stderr, stdout } = await exec(getUrlCmd, { cwd: __dirname });
16 | if (stderr) throw new Error(stderr);
17 | const response = JSON.parse(stdout);
18 | const { url } = response.result;
19 | console.log(`Command returned with response: ${url}`);
20 | return url;
21 | } catch (err) {
22 | throw err;
23 | }
24 | }
25 |
26 | /**
27 | * Main script entry point - generate a property file with the correct salesforce login url:
28 | * 1. get the org login url
29 | * 2. overwrite property file with the url returned in step 1
30 | */
31 | async function generateLoginUrl() {
32 | try {
33 | const url = await getScratchOrgLoginUrl();
34 | const template = `# DO NOT CHECK THIS FILE IN WITH PERSONAL INFORMATION SAVED
35 | SALESFORCE_LOGIN_URL=${url}
36 | SALESFORCE_LOGIN_TIME=${new Date().getTime()}`;
37 | writeFileSync(DOTENV_FILEPATH, template);
38 | console.log(`Property .env file successfully generated in ${DOTENV_FILEPATH}`);
39 | } catch (err) {
40 | console.error(err);
41 | }
42 | }
43 |
44 | generateLoginUrl();
45 |
--------------------------------------------------------------------------------
/scripts/soql/account.soql:
--------------------------------------------------------------------------------
1 | // Use .soql files to store SOQL queries.
2 | // You can execute queries in VS Code by selecting the
3 | // query text and running the command:
4 | // SFDX: Execute SOQL Query with Currently Selected Text
5 |
6 | SELECT Id, Name FROM Account
7 |
--------------------------------------------------------------------------------
/sfdx-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageDirectories": [
3 | {
4 | "path": "force-app",
5 | "default": true
6 | }
7 | ],
8 | "name": "salesforce-utam-e2e-testing",
9 | "namespace": "",
10 | "sfdcLoginUrl": "https://login.salesforce.com",
11 | "sourceApiVersion": "63.0"
12 | }
13 |
--------------------------------------------------------------------------------
/utam.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // file masks for utam page objects
3 | pageObjectsFileMask: ['force-app/**/__utam__/**/*.utam.json'],
4 | // remap custom elements imports
5 | alias: {
6 | 'utam-sfdx/': 'salesforce-utam-e2e-testing/'
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/wdio.conf.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const { UtamWdioService } = require('wdio-utam-service');
4 | // use prefix 'DEBUG=true' to run test in debug mode
5 | const { DEBUG } = process.env;
6 | const TIMEOUT = DEBUG ? 60 * 1000 * 30 : 60 * 1000;
7 |
8 | exports.config = {
9 | runner: 'local',
10 | specs: ['force-app/test/**/*.spec.js'],
11 | maxInstances: 1,
12 | capabilities: [
13 | {
14 | maxInstances: 1,
15 | browserName: 'chrome',
16 | 'goog:chromeOptions': {
17 | // to run chrome headless the following flags are required
18 | // (see https://developers.google.com/web/updates/2017/04/headless-chrome)
19 | // to deactivate the headless mode for local development and testing, please comment out the following line
20 | args: ['--headless=new']
21 | }
22 | }
23 | ],
24 | logLevel: 'debug',
25 | bail: 0,
26 | // timeout for all waitFor commands
27 | waitforTimeout: TIMEOUT,
28 | connectionRetryTimeout: 120000,
29 | connectionRetryCount: 3,
30 | automationProtocol: 'webdriver',
31 | services: [
32 | 'chromedriver',
33 | [
34 | UtamWdioService,
35 | {
36 | implicitTimeout: 0,
37 | injectionConfigs: ['salesforce-pageobjects/ui-global-components.config.json']
38 | }
39 | ]
40 | ],
41 | framework: 'jasmine',
42 | reporters: ['spec'],
43 | jasmineOpt: {
44 | // max execution time for a script, set to 5 min
45 | defaultTimeoutInterval: 1000 * 60 * 5
46 | }
47 | };
48 |
--------------------------------------------------------------------------------