Skip to content

Commit d0883f6

Browse files
✅ Add Salesforce E2E tests (#4761)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8253604 commit d0883f6

16 files changed

Lines changed: 445 additions & 157 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# BROWSERSTACK CREDENTIALS
22
BS_USERNAME=xxx
33
BS_ACCESS_KEY=xxx
4+
5+
# Salesforce credentials
6+
SF_LWC_CLIENT_ID=xxx
7+
SF_LWC_USERNAME=xxx
8+
SF_LWC_INSTANCE_URL=xxx
9+
SF_LWC_JWT_PRIVATE_KEY_B64=xxx

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"build": "yarn workspaces foreach --all --parallel --topological-dev run build",
1616
"build:bundle": "yarn workspaces foreach --all --parallel run build:bundle",
1717
"build:apps": "node scripts/build/build-test-apps.ts",
18+
"salesforce:deploy-app": "yarn workspace @datadog/browser-rum-slim build:bundle && node --env-file-if-exists=.env scripts/salesforce-lwc-app.ts deploy-app",
19+
"salesforce:get-url": "node --env-file-if-exists=.env scripts/salesforce-lwc-app.ts get-url",
1820
"build:docs:json": "typedoc --logLevel Verbose --json ./generated-docs.json",
1921
"build:docs:html": "typedoc --out ./generated-docs",
2022
"serve:docs": "yarn build:docs:html && npx http-server ./generated-docs -p 8080 -o",

scripts/build/build-test-apps.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const APPS: AppConfig[] = [
3535
{ name: 'vue-router-app' },
3636
{ name: 'nuxt-app' },
3737
{ name: 'instrumentation-overhead' },
38+
{ name: 'sf-lwc-app', builderFn: buildSalesforceLwcApp },
3839

3940
// React Router apps
4041
{ name: 'react-router-app' },
@@ -161,6 +162,14 @@ async function buildApp(appName: string) {
161162
}
162163
}
163164

165+
function buildSalesforceLwcApp() {
166+
const sourceBundle = 'packages/browser-rum-slim/bundle/datadog-rum-slim.js'
167+
const targetBundle = 'test/apps/sf-lwc-app/force-app/main/default/staticresources/datadog_rum_slim.js'
168+
169+
printLog('Building app at test/apps/sf-lwc-app...')
170+
fs.copyFileSync(sourceBundle, targetBundle)
171+
}
172+
164173
async function buildReactRouterV6App() {
165174
await buildGeneratedApp('react-router-app', 'react-router-v6-app', async (appPath) => {
166175
await modifyFile(path.join(appPath, 'package.json'), (content: string) =>

scripts/lib/secrets.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ export function getBrowserStackAccessKey(): string {
8888
return getSecretKey('ci.browser-sdk.bs_access_key')
8989
}
9090

91+
export function getSfLwcClientId(): string {
92+
return process.env.SF_LWC_CLIENT_ID ?? getSecretKey('ci.browser-sdk.sf_lwc_client_id')
93+
}
94+
95+
export function getSfLwcUsername(): string {
96+
return process.env.SF_LWC_USERNAME ?? getSecretKey('ci.browser-sdk.sf_lwc_username')
97+
}
98+
99+
export function getSfLwcInstanceUrl(): string {
100+
return process.env.SF_LWC_INSTANCE_URL ?? getSecretKey('ci.browser-sdk.sf_lwc_instance_url')
101+
}
102+
103+
export function getSfLwcJwtPrivateKey(): string {
104+
return process.env.SF_LWC_JWT_PRIVATE_KEY_B64 ?? getSecretKey('ci.browser-sdk.sf_lwc_jwt_private_key_b64')
105+
}
106+
91107
function getSecretKey(name: string): string {
92108
return command`
93109
aws ssm get-parameter --region=us-east-1 --with-decryption --query=Parameter.Value --out=text --name=${name}

scripts/salesforce-lwc-app.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2+
import { dirname, resolve } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import { tmpdir } from 'node:os'
5+
import { parseArgs } from 'node:util'
6+
7+
import { printLog, runMain } from './lib/executionUtils.ts'
8+
import { getSfLwcClientId, getSfLwcInstanceUrl, getSfLwcJwtPrivateKey, getSfLwcUsername } from './lib/secrets.ts'
9+
import { command } from './lib/command.ts'
10+
11+
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
12+
const salesforceAppDir = resolve(repositoryRoot, 'test/apps/sf-lwc-app')
13+
const SALESFORCE_HOME_PATH = '/lightning/app/c__SF_LWC_App/page/home'
14+
const TARGET_ORG = 'sf-lwc-ci'
15+
16+
runMain(() => {
17+
const { values, positionals } = parseArgs({
18+
allowPositionals: true,
19+
options: {
20+
help: {
21+
type: 'boolean',
22+
short: 'h',
23+
},
24+
},
25+
})
26+
27+
if (values.help) {
28+
showUsageAndExit()
29+
}
30+
31+
if (positionals.length !== 1) {
32+
throw new Error('Usage: node scripts/salesforce-lwc-app.ts <deploy-app|get-url>')
33+
}
34+
35+
const commandName = positionals[0]
36+
37+
switch (commandName) {
38+
// Deploy the app to the Salesforce org. To be done only when the app is updated.
39+
case 'deploy-app':
40+
deployApp()
41+
break
42+
// Get the authenticated URL of the app.
43+
case 'get-url':
44+
printSalesforceLwcUrl()
45+
break
46+
default:
47+
throw new Error(`Unknown command "${commandName ?? ''}". Expected: deploy-app|get-url`)
48+
}
49+
})
50+
51+
function showUsageAndExit() {
52+
console.log('Usage: node scripts/salesforce-lwc-app.ts <deploy-app|get-url>')
53+
process.exit(0)
54+
}
55+
56+
function authenticate(targetOrg: string) {
57+
// Temporary directory holding the JWT private key for the duration of authentication.
58+
// Using a unique temp dir avoids collisions when multiple CI jobs run in parallel.
59+
const keyDirectory = mkdtempSync(resolve(tmpdir(), 'sf-lwc-jwt-'))
60+
const serverKeyPath = resolve(keyDirectory, 'server.key')
61+
62+
try {
63+
writeFileSync(serverKeyPath, Buffer.from(getSfLwcJwtPrivateKey(), 'base64').toString('utf8'), { mode: 0o600 })
64+
// writeFileSync mode can be masked by the process umask; chmodSync guarantees owner-only access.
65+
chmodSync(serverKeyPath, 0o600)
66+
67+
printLog(`Authenticating Salesforce CLI alias ${targetOrg}...`)
68+
command`sf org login jwt --client-id ${getSfLwcClientId()} --jwt-key-file ${serverKeyPath} --username ${getSfLwcUsername()} --instance-url ${getSfLwcInstanceUrl()} --alias ${targetOrg}`
69+
.withCurrentWorkingDirectory(salesforceAppDir)
70+
.withLogs()
71+
.run()
72+
printLog(`Salesforce CLI authenticated as ${targetOrg}.`)
73+
} finally {
74+
rmSync(keyDirectory, { recursive: true, force: true })
75+
}
76+
}
77+
78+
function deployApp() {
79+
authenticate(TARGET_ORG)
80+
81+
printLog(`Deploying Salesforce LWC app to ${TARGET_ORG}...`)
82+
command`sf project deploy start --target-org ${TARGET_ORG} --source-dir force-app --ignore-conflicts --concise`
83+
.withCurrentWorkingDirectory(salesforceAppDir)
84+
.withLogs()
85+
.run()
86+
printLog('Salesforce LWC app deployed.')
87+
}
88+
89+
function printSalesforceLwcUrl(): void {
90+
const path = new URL(SALESFORCE_HOME_PATH, 'https://salesforce.local')
91+
92+
authenticate(TARGET_ORG)
93+
94+
command`sf org open --target-org ${TARGET_ORG} --path ${path.pathname}${path.search} --url-only`
95+
.withCurrentWorkingDirectory(salesforceAppDir)
96+
.withLogs()
97+
.run()
98+
}

test/apps/sf-lwc-app/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.sf
22
.sfdx
3-
/force-app/main/default/staticresources/datadog_rum_slim.js
3+
/force-app/main/default/staticresources/datadog_rum_slim.js

test/apps/sf-lwc-app/README.md

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,59 @@ This app is Lightning-only.
1111
- A `Product Explorer` app page with three hardcoded editable products
1212
- `c:datadogInit` in the utility bar, backed by the `datadog_rum_slim` static resource
1313

14-
## Deploy
14+
## Authentication
1515

16-
From this directory:
16+
The Salesforce flow uses the Salesforce CLI with a JWT keypair. There is no separate manual auth step: `yarn salesforce:deploy-app` and `yarn salesforce:get-url` always (re-)authenticate the `sf-lwc-ci` alias before running, since the JWT private key file used for authentication is deleted right after login and can't be reused to refresh a cached session.
17+
18+
Credentials are set as CI variables.
19+
20+
For local overrides, set the matching environment variables from `.env.example`:
21+
22+
## Initial App Deploy
23+
24+
This app runs from Salesforce metadata already deployed to
25+
the Salesforce org, so any change to that metadata (Apex, LWC markup/config, permission sets, etc.) requires a full
26+
redeploy to take effect.
27+
28+
For E2E testing, deployment is not necesary since we will override the deployed rum_slim bundle with Playwright.
29+
30+
```sh
31+
yarn salesforce:deploy-app
32+
```
33+
34+
This builds the local RUM slim bundle, copies it to the stable `datadog_rum_slim` static resource, and deploys the app metadata.
35+
36+
## Local Bundle
37+
38+
Regular test runs do not deploy the current SDK bundle to Salesforce.
39+
Build the test apps from the repository root instead:
40+
41+
```sh
42+
yarn build:apps --app sf-lwc-app
43+
```
44+
45+
This copies the locally built RUM slim bundle into the ignored stable `datadog_rum_slim` static resource file.
46+
Playwright fulfills Salesforce static resource requests with this local file during E2E tests.
47+
48+
## Open The App
49+
50+
Print an authenticated URL for the app:
1751

1852
```sh
19-
yarn run setup -o engrumdev --ignore-conflicts
53+
yarn salesforce:get-url
2054
```
2155

22-
The setup script copies the local RUM slim bundle into the static resource, deploys the app, assigns the app permission set to the target user, and prints the app-specific Home URL.
56+
The printed URL is authenticated and should be treated as sensitive.
57+
E2E tests don't use this script: they build their own authenticated URL via the JWT/REST flow in
58+
`test/e2e/lib/framework/buildSalesforceLwcUrl.ts`, and inject the RUM configuration on the page as
59+
`window.RUM_CONFIGURATION`.
60+
61+
## Run E2E Tests
2362

24-
If prompted for a user. Log into 1Password and use `beltran.bulbarella@datadoghq.com.engrumdev`
63+
Build the SDK and test apps, then run the Salesforce scenario:
64+
65+
```sh
66+
yarn build
67+
yarn build:apps --app sf-lwc-app
68+
yarn test:e2e --project=chromium --grep salesforce
69+
```

test/apps/sf-lwc-app/force-app/main/default/lwc/customActionButtons/customActionButtons.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export default class CustomActionButtons extends LightningElement {
111111
}
112112

113113
getResourceTestUrl(token) {
114-
return `https://sample-json-api.com/products/1?${token}`
114+
return `${window.location.origin}/services/data/?${token}`
115115
}
116116

117117
getComposedPathNames(event) {

test/apps/sf-lwc-app/force-app/main/default/lwc/datadogInit/datadogInit.js

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,10 @@ import { loadScript } from 'lightning/platformResourceLoader'
66
let datadogInitialization
77
let lastStartedUrl
88

9-
const DATADOG_PARAMS = ['c__applicationId', 'c__clientToken', 'c__env', 'c__service', 'c__site']
10-
11-
const cleanUrl = (url) => {
12-
const cleanUrl = new URL(url, window.location.origin)
13-
DATADOG_PARAMS.forEach((param) => cleanUrl.searchParams.delete(param))
14-
return cleanUrl.href
15-
}
16-
17-
const defaultDatadogRumConfig = {
18-
trackViewsManually: true,
19-
trackEarlyRequests: true,
20-
trackLongTasks: true,
21-
trackResources: true,
22-
trackUserInteractions: true,
23-
beforeSend: (event) => {
24-
if (event.view) {
25-
const sanitizedViewUrl = new URL(cleanUrl(event.view.url))
26-
event.view.url = sanitizedViewUrl.href
27-
event.view.name = sanitizedViewUrl.pathname + sanitizedViewUrl.search + sanitizedViewUrl.hash
28-
}
29-
if (event.resource?.url) {
30-
event.resource.url = cleanUrl(event.resource.url)
31-
}
32-
},
9+
const defaultInitConfiguration = {
10+
applicationId: 'xxx',
11+
clientToken: 'xxx',
12+
site: 'datadoghq.com',
3313
}
3414

3515
export default class DatadogInit extends NavigationMixin(LightningElement) {
@@ -70,23 +50,8 @@ export default class DatadogInit extends NavigationMixin(LightningElement) {
7050

7151
loadDatadogRum() {
7252
return loadScript(this, datadogRumSlim).then(() => {
73-
const searchParams = new URLSearchParams(window.location.search)
74-
const applicationId = searchParams.get('c__applicationId')
75-
const clientToken = searchParams.get('c__clientToken')
76-
77-
if (!applicationId || !clientToken) {
78-
window.console.warn('Datadog RUM not initialized: missing c__applicationId or c__clientToken')
79-
return
80-
}
81-
82-
window.DD_RUM.init({
83-
applicationId,
84-
clientToken,
85-
env: searchParams.get('c__env'),
86-
service: searchParams.get('c__service'),
87-
site: searchParams.get('c__site'),
88-
...defaultDatadogRumConfig,
89-
})
53+
window.DD_RUM.setGlobalContext(window.RUM_CONTEXT)
54+
window.DD_RUM.init({ ...defaultInitConfiguration, ...window.RUM_CONFIGURATION })
9055
lastStartedUrl = window.location.pathname + window.location.search + window.location.hash
9156
window.DD_RUM.startView({
9257
name: lastStartedUrl,

test/apps/sf-lwc-app/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"name": "sf-lwc-app",
33
"private": true,
4-
"description": "Salesforce Lightning app for browser-sdk Salesforce testing",
5-
"scripts": {
6-
"setup": "node scripts/setup.mjs"
7-
}
4+
"description": "Salesforce Lightning app for browser-sdk Salesforce testing"
85
}

0 commit comments

Comments
 (0)