├── .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 | ![GitHub CI](https://github.com/svierk/salesforce-utam-e2e-testing/actions/workflows/ci.yml/badge.svg) 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 | custom-slider 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 | 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 | --------------------------------------------------------------------------------