diff --git a/.gitignore b/.gitignore
index 19d04cedd..71c227dbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,4 @@ local.log
**/*.gen.ts
-.env
\ No newline at end of file
+.env
diff --git a/lib/index.umd.spec.ts b/lib/index.umd.spec.ts
new file mode 100644
index 000000000..c73d99144
--- /dev/null
+++ b/lib/index.umd.spec.ts
@@ -0,0 +1,23 @@
+import { expect, describe, it } from 'vitest';
+
+import * as optimizely from './index.browser';
+
+type OptimizelySdk = typeof optimizely;
+
+declare global {
+ interface Window {
+ optimizelySdk: OptimizelySdk;
+ }
+}
+
+describe('UMD Bundle', () => {
+ // these are just intial tests to check the UMD bundle is loaded correctly
+ // we will add more comprehensive umd tests later
+ it('should have optimizelySdk on the window object', () => {
+ expect(window.optimizelySdk).toBeDefined();
+ });
+
+ it('should export createInstance function', () => {
+ expect(typeof window.optimizelySdk.createInstance).toBe('function');
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 7db4331a8..a55aef60f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,12 +29,12 @@
"@typescript-eslint/parser": "^5.33.0",
"@vitest/browser": "3.2.4",
"chai": "^4.2.0",
- "coveralls-next": "^4.2.0",
+ "coveralls-next": "^5.0.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-prettier": "^3.1.2",
- "happy-dom": "^16.6.0",
+ "happy-dom": "^20.0.11",
"jiti": "^2.4.1",
"karma": "^6.4.0",
"karma-browserstack-launcher": "^1.5.1",
@@ -62,7 +62,7 @@
"vite": "^6.4.1",
"vitest": "^3.2.4",
"webdriverio": "^9.21.0",
- "webpack": "^5.74.0"
+ "webpack": "^5.94.0"
},
"engines": {
"node": ">=18.0.0"
@@ -267,7 +267,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -277,7 +276,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -297,7 +295,6 @@
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
@@ -614,7 +611,6 @@
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
@@ -1348,23 +1344,13 @@
"camelcase": "^5.3.1",
"find-up": "^4.1.0",
"get-package-type": "^0.1.0",
- "js-yaml": "^3.13.1",
+ "js-yaml": "^4.1.1",
"resolve-from": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
- "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
"node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -1379,20 +1365,6 @@
"node": ">=8"
}
},
- "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
- "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
"node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -1808,7 +1780,6 @@
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
@@ -2756,6 +2727,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/whatwg-mimetype": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz",
@@ -4081,13 +4059,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/b4a": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
@@ -5010,19 +4981,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -5147,13 +5105,12 @@
}
},
"node_modules/coveralls-next": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.2.tgz",
- "integrity": "sha512-Tw1TKXV0+aEfOgRYBN97RtEZlrLxBiZKFkngsupONkJwy0uYQNbB6VfAEnGnOUa5WkW5sBhjGB2tWha6ULrYkw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-5.0.0.tgz",
+ "integrity": "sha512-RCj6Oflf6iQtN3Q5b0SSemEbQBzeBjQlLUrc3bfNECTy83hMJA9krdNZ5GTRm7Jpbyo92yKUbQDP5FYlWcL5sA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "form-data": "4.0.4",
"js-yaml": "4.1.0",
"lcov-parse": "1.0.0",
"log-driver": "1.2.7",
@@ -5166,6 +5123,19 @@
"node": ">=18"
}
},
+ "node_modules/coveralls-next/node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -5382,16 +5352,6 @@
"node": ">= 14"
}
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -5911,22 +5871,6 @@
"node": ">= 0.4"
}
},
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -6781,23 +6725,6 @@
"node": ">=8.0.0"
}
},
- "node_modules/form-data": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
- "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/formatio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
@@ -7238,19 +7165,37 @@
"license": "MIT"
},
"node_modules/happy-dom": {
- "version": "16.8.1",
- "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.8.1.tgz",
- "integrity": "sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw==",
+ "version": "20.0.11",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.0.11.tgz",
+ "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "webidl-conversions": "^7.0.0",
+ "@types/node": "^20.0.0",
+ "@types/whatwg-mimetype": "^3.0.2",
"whatwg-mimetype": "^3.0.0"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
}
},
+ "node_modules/happy-dom/node_modules/@types/node": {
+ "version": "20.19.27",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
+ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/happy-dom/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -8352,9 +8297,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11546,7 +11491,7 @@
"@babel/code-frame": "^7.5.5",
"jest-worker": "^24.9.0",
"rollup-pluginutils": "^2.8.2",
- "serialize-javascript": "^4.0.0",
+ "serialize-javascript": "^6.0.2",
"terser": "^4.6.2"
},
"peerDependencies": {
@@ -11584,16 +11529,6 @@
"node": ">= 6"
}
},
- "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
- "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/rollup-plugin-terser/node_modules/supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
@@ -12478,13 +12413,6 @@
"node": ">= 10.x"
}
},
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true,
- "license": "BSD-3-Clause"
- },
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -14068,16 +13996,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/webidl-conversions": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/webpack": {
"version": "5.104.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
diff --git a/package.json b/package.json
index f667384cf..9ea6a2e21 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,9 @@
"test-browser": "node ./scripts/run-browser-tests.js",
"test-browser-local": "USE_LOCAL_BROWSER=true node ./scripts/run-browser-tests.js",
"test-browser-browserstack": "USE_LOCAL_BROWSER=false node ./scripts/run-browser-tests.js",
+ "test-umd": "node ./scripts/run-umd-tests.js",
+ "test-umd-local": "USE_LOCAL_BROWSER=true node ./scripts/run-umd-tests.js",
+ "test-umd-browserstack": "USE_LOCAL_BROWSER=false node ./scripts/run-umd-tests.js",
"test-mocha": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register -r tsconfig-paths/register -r lib/tests/exit_on_unhandled_rejection.js 'lib/**/*.tests.ts' 'lib/**/*.tests.js'",
"test": "npm run test-mocha && npm run test-vitest",
"posttest": "npm run lint",
@@ -74,7 +77,7 @@
"prebuild": "npm run clean",
"build": "npm run validate-platform-isolation && tsc --noEmit && npm run genmsg && rollup -c && cp dist/index.browser.d.ts dist/index.d.ts",
"build:win": "tsc --noEmit && npm run genmsg && rollup -c && type nul > dist/optimizely.lite.es.d.ts && type nul > dist/optimizely.lite.es.min.d.ts && type nul > dist/optimizely.lite.min.d.ts",
- "build-browser-umd": "rollup -c --config-umd",
+ "build-browser-umd": "npm run validate-platform-isolation && tsc --noEmit && npm run genmsg && rollup -c --config-umd",
"coveralls": "nyc --reporter=lcov npm test",
"prepare": "npm run build",
"prepublishOnly": "npm test",
@@ -117,12 +120,12 @@
"@typescript-eslint/parser": "^5.33.0",
"@vitest/browser": "3.2.4",
"chai": "^4.2.0",
- "coveralls-next": "^4.2.0",
+ "coveralls-next": "^5.0.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-prettier": "^3.1.2",
- "happy-dom": "^16.6.0",
+ "happy-dom": "^20.0.11",
"jiti": "^2.4.1",
"karma": "^6.4.0",
"karma-browserstack-launcher": "^1.5.1",
@@ -149,8 +152,8 @@
"typescript": "^4.7.4",
"vite": "^6.4.1",
"vitest": "^3.2.4",
- "webdriverio": "^9.21.0",
- "webpack": "^5.74.0"
+ "webpack": "^5.94.0",
+ "webdriverio": "^9.21.0"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": ">=1.0.0 <3.0.0",
diff --git a/scripts/run-umd-tests.js b/scripts/run-umd-tests.js
new file mode 100755
index 000000000..9aafa9fe8
--- /dev/null
+++ b/scripts/run-umd-tests.js
@@ -0,0 +1,294 @@
+#!/usr/bin/env node
+
+/**
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const { execSync } = require('child_process');
+const browserstack = require('browserstack-local');
+const fs = require('fs');
+const path = require('path');
+
+// Browser configurations grouped by browser name
+const BROWSER_CONFIGS = {
+ chrome: [
+ { name: 'chrome-102-windows', browserVersion: '102', os: 'Windows', osVersion: '11' },
+ { name: 'chrome-latest-windows', browserVersion: 'latest', os: 'Windows', osVersion: '11' },
+ ],
+ firefox: [
+ { name: 'firefox-91-windows', browserVersion: '91', os: 'Windows', osVersion: '11' },
+ { name: 'firefox-latest-windows', browserVersion: 'latest', os: 'Windows', osVersion: '11' },
+ ],
+ edge: [
+ { name: 'edge-89-windows', browserVersion: '89', os: 'Windows', osVersion: '11' },
+ { name: 'edge-latest-windows', browserVersion: 'latest', os: 'Windows', osVersion: '11' },
+ ],
+ safari: [
+ { name: 'safari-monterey', os: 'OS X', osVersion: 'Monterey' },
+ { name: 'safari-sequoia', os: 'OS X', osVersion: 'Sequoia' },
+ ]
+};
+
+// Determine if we should use local browser or BrowserStack
+// Priority: USE_LOCAL_BROWSER env var, then check for BrowserStack credentials
+let useLocalBrowser = process.env.USE_LOCAL_BROWSER === 'true';
+
+if (!useLocalBrowser) {
+ // Check for BrowserStack credentials
+ const username = process.env.BROWSERSTACK_USERNAME || process.env.BROWSER_STACK_USERNAME;
+ const accessKey = process.env.BROWSERSTACK_ACCESS_KEY || process.env.BROWSER_STACK_ACCESS_KEY;
+
+ console.log('\n' + '='.repeat(80));
+ console.log('BrowserStack Credentials Check:');
+ console.log('='.repeat(80));
+ console.log(`BROWSERSTACK_USERNAME: ${username ? '✓ Available' : '✗ Not found'}`);
+ console.log(`BROWSERSTACK_ACCESS_KEY: ${accessKey ? '✓ Available' : '✗ Not found'}`);
+ console.log('='.repeat(80) + '\n');
+
+ if (!username || !accessKey) {
+ console.log('BrowserStack credentials not found - falling back to local browser mode');
+ useLocalBrowser = true;
+ }
+}
+
+
+let bs_local = null;
+
+function startTunnel(localIdentifier) {
+ const accessKey = process.env.BROWSERSTACK_ACCESS_KEY || process.env.BROWSER_STACK_ACCESS_KEY;
+
+ console.log(`Starting BrowserStack Local tunnel with identifier: ${localIdentifier}...`);
+ bs_local = new browserstack.Local();
+ const bsLocalArgs = {
+ key: accessKey,
+ force: true,
+ forceLocal: true,
+ // Enable verbose logging to debug tunnel issues
+ verbose: true,
+ // Use the provided identifier for parallel tunnel support
+ localIdentifier: localIdentifier,
+ };
+
+ return new Promise((resolve, reject) => {
+ bs_local.start(bsLocalArgs, (error) => {
+ if (error) {
+ console.error('Error starting BrowserStack Local:', error);
+ reject(error);
+ } else {
+ console.log('BrowserStack Local tunnel started successfully');
+ console.log(`BrowserStack Local PID: ${bs_local.pid}`);
+ console.log(`Local Identifier: ${localIdentifier}`);
+ // Wait longer for tunnel to fully establish and register with BrowserStack
+ console.log('Waiting for tunnel to establish...');
+ setTimeout(() => {
+ console.log('Tunnel ready!');
+ resolve();
+ }, 10000);
+ }
+ });
+ });
+}
+
+function stopTunnel() {
+ if (!bs_local) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ bs_local.stop(() => {
+ console.log('BrowserStack Local tunnel stopped');
+ resolve();
+ });
+ });
+}
+
+async function runTests() {
+ let exitCode = 0;
+
+ try {
+ // Step 1: Run npm run build
+ console.log('\n' + '='.repeat(80));
+ console.log('Building project...');
+ console.log('='.repeat(80));
+ try {
+ execSync('npm run build-browser-umd', { stdio: 'inherit' });
+ console.log('Build completed successfully!');
+ } catch (error) {
+ console.error('Failed to build project:', error.message);
+ exitCode = 1;
+ return;
+ }
+
+ // Step 2: Copy the UMD file to vitest/public/dist/
+ console.log('\n' + '='.repeat(80));
+ console.log('Copying UMD file to vitest/public/dist/...');
+ console.log('='.repeat(80));
+ try {
+ const sourceFile = path.join(process.cwd(), 'dist/optimizely.browser.umd.min.js');
+ const destDir = path.join(process.cwd(), 'vitest/public/dist');
+ const destFile = path.join(destDir, 'optimizely.browser.umd.min.js');
+
+ // Create destination directory if it doesn't exist
+ if (!fs.existsSync(destDir)) {
+ fs.mkdirSync(destDir, { recursive: true });
+ console.log(`Created directory: ${destDir}`);
+ }
+
+ // Copy the file
+ fs.copyFileSync(sourceFile, destFile);
+ console.log(`Copied ${sourceFile} to ${destFile}`);
+ } catch (error) {
+ console.error('Failed to copy UMD file:', error.message);
+ exitCode = 1;
+ return;
+ }
+
+ // Patch Vitest viewport command to prevent WebDriver Bidi errors
+ console.log('\n' + '='.repeat(80));
+ console.log('Patching Vitest viewport command...');
+ console.log('='.repeat(80));
+ try {
+ execSync('node ./scripts/patch-vitest-viewport.js', { stdio: 'inherit' });
+ } catch (error) {
+ console.error('Failed to patch Vitest viewport command:', error.message);
+ exitCode = 1;
+ return;
+ }
+
+ // Get browser name from environment variable (default to chrome)
+ const browserName = (process.env.TEST_BROWSER || 'chrome').toLowerCase();
+
+ let configs;
+
+ if (useLocalBrowser) {
+ configs = [{
+ name: `${browserName}`,
+ }];
+ console.log('Local browser mode: using local browser installation');
+ } else {
+ // For BrowserStack, use the defined configs
+ configs = BROWSER_CONFIGS[browserName];
+ if (!configs || configs.length === 0) {
+ console.error(`Error: No configurations found for browser '${browserName}'`);
+ console.error(`Available browsers: ${Object.keys(BROWSER_CONFIGS).join(', ')}`);
+ exitCode = 1;
+ return;
+ }
+ }
+
+ // Only start tunnel if using BrowserStack
+ let localIdentifier;
+ if (!useLocalBrowser) {
+ // Generate a random identifier for parallel tunnel support (100000-900000)
+ localIdentifier = Math.floor(Math.random() * 800000) + 100000;
+ localIdentifier = localIdentifier.toString();
+ await startTunnel(localIdentifier);
+ } else {
+ console.log('Using local browser mode - no BrowserStack connection needed');
+ }
+
+ console.log('\n' + '='.repeat(80));
+ console.log(`Running UMD tests for browser: ${browserName}`);
+ console.log(`Total configurations: ${configs.length}`);
+ console.log('='.repeat(80) + '\n');
+
+ const results = [];
+
+ // Run each config serially
+ for (const config of configs) {
+ console.log(`\n${'='.repeat(80)}`);
+ console.log(`Running: ${config.name}`);
+ console.log(`Browser: ${browserName}${config.browserVersion ? ` ${config.browserVersion}` : ''}`);
+ console.log(`OS: ${config.os} ${config.osVersion}`);
+ console.log('='.repeat(80));
+
+ // Set environment variables for this config
+ const env = {
+ ...process.env,
+ TEST_BROWSER: browserName,
+ TEST_BROWSER_VERSION: config.browserVersion,
+ TEST_OS_NAME: config.os,
+ TEST_OS_VERSION: config.osVersion,
+ // Pass the local identifier to vitest config for BrowserStack capabilities
+ BROWSERSTACK_LOCAL_IDENTIFIER: localIdentifier,
+ };
+
+
+ try {
+ console.log('Starting vitest UMD test...');
+ // Run vitest with the UMD config
+ execSync('npm run test-vitest -- --config vitest.umd.config.mts', {
+ stdio: 'inherit',
+ env,
+ });
+
+ console.log(`\n✓ ${config.name} passed!`);
+ results.push({ config: config.name, success: true });
+ } catch (error) {
+ console.error(`\n✗ ${config.name} failed`);
+ if (error.message) {
+ console.error('Error message:', error.message);
+ }
+ results.push({ config: config.name, success: false });
+ }
+ }
+
+ // Print summary
+ console.log('\n' + '='.repeat(80));
+ console.log(`UMD test summary for ${browserName}:`);
+ console.log('='.repeat(80));
+
+ const failures = [];
+ const successes = [];
+
+ results.forEach(({ config, success }) => {
+ if (success) {
+ successes.push(config);
+ console.log(`✓ ${config}: PASSED`);
+ } else {
+ failures.push(config);
+ console.error(`✗ ${config}: FAILED`);
+ }
+ });
+
+ console.log('='.repeat(80));
+ console.log(`Total: ${results.length} configurations`);
+ console.log(`Passed: ${successes.length}`);
+ console.log(`Failed: ${failures.length}`);
+ console.log('='.repeat(80));
+
+ // Set exit code based on results
+ if (failures.length > 0) {
+ console.error(`\nSome ${browserName} configurations failed. See above for details.`);
+ exitCode = 1;
+ } else {
+ console.log(`\nAll ${browserName} configurations passed!`);
+ exitCode = 0;
+ }
+ } finally {
+ // Only stop tunnel if using BrowserStack
+ if (!useLocalBrowser) {
+ await stopTunnel();
+ }
+
+ // Exit after tunnel is properly closed
+ process.exit(exitCode);
+ }
+}
+
+// Run the tests
+runTests().catch((error) => {
+ console.error('Fatal error:', error);
+ process.exit(1);
+});
diff --git a/vitest.browser.config.mts b/vitest.browser.config.mts
index 8588b522b..7bc7d43c4 100644
--- a/vitest.browser.config.mts
+++ b/vitest.browser.config.mts
@@ -42,7 +42,7 @@ const testOsVersion = process.env.TEST_OS_VERSION;
// Build local browser capabilities
function buildLocalCapabilities() {
return {
- testBrowser,
+ browserName: testBrowser,
'goog:chromeOptions': {
args: [
'--disable-blink-features=AutomationControlled',
@@ -139,6 +139,8 @@ export default defineConfig({
target: 'es2015',
},
},
+ // Serve public files from vitest/public directory
+ publicDir: 'vitest/public',
server: {
host: '0.0.0.0',
// for safari, browserstack redirects localhost to bs-local.com
@@ -147,8 +149,7 @@ export default defineConfig({
test: {
isolate: false,
fileParallelism: true,
- // Reduce concurrency for BrowserStack to minimize tunnel load and WebSocket connection issues
- maxConcurrency: useLocalBrowser ? 5 : 1,
+ maxConcurrency: 5,
onConsoleLog: () => true,
browser: {
enabled: true,
@@ -165,6 +166,7 @@ export default defineConfig({
exclude: [
'lib/**/*.react_native.spec.ts',
'lib/**/*.node.spec.ts',
+ 'lib/*.umd.spec.ts'
],
typecheck: {
enabled: true,
diff --git a/vitest.config.mts b/vitest.config.mts
index 178edb642..256405faf 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -27,6 +27,7 @@ export default defineConfig({
onConsoleLog: () => true,
environment: 'happy-dom',
include: ['lib/**/*.spec.ts'],
+ exclude: ['lib/*.umd.spec.ts'],
typecheck: {
enabled: true,
tsconfig: 'tsconfig.spec.json',
diff --git a/vitest.umd.config.mts b/vitest.umd.config.mts
new file mode 100644
index 000000000..de9090d02
--- /dev/null
+++ b/vitest.umd.config.mts
@@ -0,0 +1,169 @@
+/**
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///
+import path from 'path';
+import { defineConfig } from 'vitest/config'
+import { umdPlugin } from './vitest/umd-plugin';
+
+// Check if we should use local browser instead of BrowserStack
+const useLocalBrowser = process.env.USE_LOCAL_BROWSER === 'true';
+
+
+// Get browser configuration from TEST_* environment variables
+const testBrowser = process.env.TEST_BROWSER || 'chrome';
+const testBrowserVersion = process.env.TEST_BROWSER_VERSION;
+const testOsName = process.env.TEST_OS_NAME;
+const testOsVersion = process.env.TEST_OS_VERSION;
+
+// const browserConfig = {
+// name: testBrowser,
+// browserName: testBrowser,
+// browserVersion: testBrowserVersion,
+// os: testOsName,
+// osVersion: testOsVersion,
+// };
+
+// const browserConfigs = [browserConfig];
+
+// Build local browser capabilities
+function buildLocalCapabilities() {
+ return {
+ browserName: testBrowser,
+ 'goog:chromeOptions': {
+ args: [
+ '--disable-blink-features=AutomationControlled',
+ '--disable-dev-shm-usage',
+ '--no-sandbox',
+ ],
+ },
+ };
+}
+
+// Build BrowserStack capabilities
+function buildBrowserStackCapabilities() {
+ const localIdentifier = process.env.BROWSERSTACK_LOCAL_IDENTIFIER;
+
+ return {
+ browserName: testBrowser,
+ 'wdio:enforceWebDriverClassic': true, // this doesn't work due to vitest bug, still keeping here for future reference
+ 'goog:chromeOptions': {
+ args: [
+ '--disable-blink-features=AutomationControlled',
+ '--disable-dev-shm-usage',
+ '--no-sandbox',
+ ],
+ },
+ 'bstack:options': {
+ os: testOsName,
+ osVersion: testOsVersion,
+ browserVersion: testBrowserVersion,
+ buildName: process.env.VITEST_BUILD_NAME || 'Vitest Browser Tests',
+ projectName: 'Optimizely JavaScript SDK',
+ sessionName: `${testBrowser} ${testBrowserVersion || ''} on ${testOsName} ${testOsVersion}`,
+ local: true,
+ // Include localIdentifier for parallel tunnel support
+ ...(localIdentifier && { localIdentifier }),
+ debug: false,
+ networkLogs: false,
+ consoleLogs: 'errors' as const,
+ seleniumLogs: false,
+ idleTimeout: 900, // 15 minutes idle timeout - prevents premature session closure during long test runs
+ },
+ };
+}
+
+function buildBrowserInstances() {
+ if (useLocalBrowser) {
+ // Local browser configurations - all browsers
+ return [{
+ browser: testBrowser,
+ capabilities: buildLocalCapabilities(),
+ }];
+ } else {
+
+ const username = process.env.BROWSERSTACK_USERNAME || process.env.BROWSER_STACK_USERNAME;
+ const key = process.env.BROWSERSTACK_ACCESS_KEY || process.env.BROWSER_STACK_ACCESS_KEY;
+
+ return [{
+ browser: testBrowser,
+ user: username,
+ key: key,
+ capabilities: buildBrowserStackCapabilities(),
+ connectionRetryTimeout: 120000, // 2 minutes connection retry timeout
+ connectionRetryCount: 3, // Retry 3 times on connection failure
+ waitforTimeout: 30000, // 30 seconds wait timeout - matches test expectations
+ waitforInterval: 1000, // Poll every 1 second - faster feedback
+ keepAlive: true,
+ keepAliveInterval: 30000,
+ }];
+ }
+}
+
+export default defineConfig({
+ plugins: [
+ umdPlugin(),
+ ],
+ resolve: {
+ alias: {
+ 'error_message': path.resolve(__dirname, './lib/message/error_message'),
+ 'log_message': path.resolve(__dirname, './lib/message/log_message'),
+ },
+ },
+ esbuild: {
+ target: 'es2015',
+ format: 'esm',
+ },
+ build: {
+ target: 'es2015',
+ },
+ optimizeDeps: {
+ // Force chai to be pre-bundled with ES6 target to remove class static blocks
+ // This avoids issues with browsers that do not support class static blocks like firefox 91
+ include: ['chai'],
+ esbuildOptions: {
+ target: 'es2015',
+ },
+ },
+ // Serve public files from vitest/public directory
+ publicDir: 'vitest/public',
+ server: {
+ host: '0.0.0.0',
+ // for safari, browserstack redirects localhost to bs-local.com
+ allowedHosts: ['bs-local.com', 'localhost'],
+ },
+ test: {
+ isolate: false,
+ fileParallelism: true,
+ maxConcurrency: 5,
+ onConsoleLog: () => true,
+ browser: {
+ enabled: true,
+ provider: 'webdriverio',
+ headless: false,
+ instances: buildBrowserInstances(),
+ connectTimeout: 300000,
+ },
+ testTimeout: 60000,
+ hookTimeout: 30000,
+ include: [
+ 'lib/**/*.umd.spec.ts',
+ ],
+ typecheck: {
+ enabled: true,
+ tsconfig: 'tsconfig.spec.json',
+ },
+ },
+});
diff --git a/public/console-capture.js b/vitest/public/console-capture.js
similarity index 100%
rename from public/console-capture.js
rename to vitest/public/console-capture.js
diff --git a/vitest/umd-plugin.ts b/vitest/umd-plugin.ts
new file mode 100644
index 000000000..9c285b835
--- /dev/null
+++ b/vitest/umd-plugin.ts
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Plugin } from 'vitest/config';
+import type { IncomingMessage, ServerResponse } from 'http';
+
+export function umdPlugin(): Plugin {
+ return {
+ name: 'umd-plugin',
+ enforce: 'pre' as const,
+ configureServer(server) {
+ // Add middleware to inject umd script into HTML responses
+ server.middlewares.use((_req: IncomingMessage, res: ServerResponse, next: () => void) => {
+ const originalWrite = res.write;
+ const originalEnd = res.end;
+ const chunks: any[] = [];
+
+ // @ts-ignore
+ res.write = function(chunk: any, ..._args: any[]) {
+ chunks.push(Buffer.from(chunk));
+ return true;
+ };
+
+ // @ts-ignore
+ res.end = function(chunk: any, ...args: any[]) {
+ if (chunk) {
+ chunks.push(Buffer.from(chunk));
+ }
+
+ const buffer = Buffer.concat(chunks);
+ let body = buffer.toString('utf8');
+
+ // Inject console-capture script into HTML responses
+ if (res.getHeader('content-type')?.toString().includes('text/html')) {
+ const scriptTag = '';
+ if (body.includes('')) {
+ body = body.replace('', `${scriptTag}\n`);
+ res.setHeader('content-length', Buffer.byteLength(body));
+ }
+ }
+
+ // Restore original methods and send response
+ res.write = originalWrite;
+ res.end = originalEnd;
+ res.end(body, ...args);
+ };
+
+ next();
+ });
+
+ },
+ };
+}