Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions .github/workflows/javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,33 @@ jobs:
TEST_BROWSER: ${{ matrix.browser }}
steps:
- uses: actions/checkout@v3
# Runs the browser tests with Vitest 5 from tests/browser (see its README).
# Needs Node 22.23.4+ (Vitest 5 / webdriverio); the root project stays on Vitest 3 for Node 18.
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
cache: 'npm'
cache-dependency-path: ./package-lock.json
- name: Browser tests - ${{ matrix.browser }}
cache-dependency-path: |
./package-lock.json
./tests/browser/package-lock.json
- name: Install SDK dependencies
working-directory: .
run: |
npm install
npm run test-browser
run: npm install
- name: Install browser test dependencies
id: browser-deps
working-directory: ./tests/browser
run: npm ci
- name: Browser tests - ${{ matrix.browser }}
working-directory: ./tests/browser
run: npm run test-browser
# Builds the UMD bundle (root build-browser-umd) and tests it in the same browsers.
# Runs even if the browser tests failed, so both results are reported, but not
# if installing dependencies failed (browser-deps is skipped if the SDK install fails).
- name: UMD tests - ${{ matrix.browser }}
if: ${{ !cancelled() && steps.browser-deps.outcome == 'success' }}
working-directory: ./tests/browser
run: npm run test-umd

# crossbrowser_and_umd_unit_tests:
# runs-on: ubuntu-latest
Expand Down
10 changes: 6 additions & 4 deletions lib/utils/executor/backoff_retry_runner.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { runWithRetry } from './backoff_retry_runner';
import { advanceTimersByTime } from '../../tests/testUtils';
import { ConstantBackoff } from '../repeater/repeater';

const exhaustMicrotasks = async (loop = 100) => {
for(let i = 0; i < loop; i++) {
Expand Down Expand Up @@ -116,18 +117,19 @@ describe('runWithRetry', () => {
throw new Error('error');
};

const { result, cancelRetry } = runWithRetry(task, undefined, 100);
const { result, cancelRetry } = runWithRetry(task, new ConstantBackoff(100), 100);

for(let i = 0; i < 5; i++) {
await exhaustMicrotasks();
await advanceTimersByTime(0);
// the last iteration stops 10ms short, so the next retry is still waiting on its backoff
await advanceTimersByTime(i < 4 ? 100 : 90);
}

cancelRetry();

for(let i = 0; i < 100; i++) {
await exhaustMicrotasks();
await advanceTimersByTime(0);
await advanceTimersByTime(100);
}

try {
Expand Down
36 changes: 36 additions & 0 deletions tests/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Browser and UMD tests on Vitest 5

Runs the same browser and UMD spec files as the root setup (`npm run test-browser` / `npm run test-umd`), but with Vitest 5 instead of Vitest 3. It's a separate project because the root has to stay on Vitest 3 for Node 18. The root setup is unchanged and still works.

Requires Node 22.23.4+, 24.22.1+ or 26.12.1+ (the minimums of the locked webdriverio packages; Vitest 5 itself needs 22.12+).

```sh
cd tests/browser
npm install

npm run test-browser-local # Chrome on this machine
TEST_BROWSER=firefox npm run test-browser-local # a different local browser
npm run test-umd-local # builds the UMD bundle in the repo root first

npm run test-browser # BrowserStack if credentials are set, local otherwise
npm run test-umd
```

Same environment variables as the root setup: `TEST_BROWSER`, `USE_LOCAL_BROWSER`, `BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY`, `VITEST_REQUEST_LOGGER`, `VITEST_CONSOLE_CAPTURE`. Same browser matrix as the root setup (see `BROWSER_CONFIGS` in `run.js`).

Runs can go in parallel, for example one terminal per browser: each run starts its own BrowserStack Local tunnel with a unique identifier. (The root scripts start the tunnel with `force: true`, which kills other tunnels, so they can't run alongside each other or this project.)

Extra arguments go to Vitest, for example `node run.js browser --reporter=verbose ../../lib/core/bucketer/index.spec.ts`.

## Differences from the root (Vitest 3) setup

- No viewport patch. Vitest 5 falls back to classic WebDriver when BiDi is off.
- Each spec file runs in its own iframe (`isolate: true`). Shared iframes break `vi.mock` under Vitest 5.
- Modules are always mocked by request interception (`browser.importMapMocks: false`), the same way Vitest 3 does it and the way old browsers must.
- `clearMocks: false` plus `vitest3-compat.setup.ts` keep the Vitest 3 mock-reset behavior the specs rely on.
- Source is compiled for the oldest tested browsers, not ES2015, because Vitest 5's mock modules can't be compiled down to ES2015. Dependencies are still compiled to ES2015.
- `legacy-polyfills-plugin.ts` adds `Array.prototype.at`, `findLast`/`findLastIndex` and `Object.hasOwn` to both pages Vitest loads (the orchestrator page and the test iframe). Vitest 5's browser runtime calls them, and Edge 89/90 and Firefox 91 don't have them; without the polyfills the run hangs until it times out. The SDK code under test sees the polyfills too (`lib/` doesn't use these functions today).
- `lightningcss` is replaced by an empty stub (`stubs/lightningcss`, wired in through `overrides` in `package.json`). It's MPL-2.0, which our license policy doesn't allow. Vite 8 depends on it but only loads it for `css.transformer: 'lightningcss'` or CSS minification in production builds, neither of which happens here. If something ever loads it, the stub throws an error saying so.
- Type tests (`*.test-d.ts`) are skipped. The root `npm test` already runs them.

See the comments in `vitest.shared.mts` for details.
37 changes: 37 additions & 0 deletions tests/browser/console-capture-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright 2026, 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 'vite';
import { consoleCapturePlugin as rootConsoleCapturePlugin } from '../../vitest/console-capture-plugin';

// Vitest 5 version of the root vitest/console-capture-plugin.ts. Reuses its
// /__vitest_console__ endpoint, but injects vitest/public/console-capture.js
// through transformIndexHtml, because the root plugin's response patching
// doesn't reach the tester page under Vitest 5. (The root plugin skips pages
// that already include the script, so it is never added twice.)
export function consoleCapturePlugin(): Plugin {
return {
...(rootConsoleCapturePlugin() as unknown as Plugin),
transformIndexHtml() {
return [
{
tag: 'script',
attrs: { src: '/console-capture.js' },
injectTo: 'head-prepend',
},
];
},
};
}
102 changes: 102 additions & 0 deletions tests/browser/legacy-polyfills-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright 2026, 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 'vite';

// Vitest 5's in-browser runtime (the tester page and the pre-bundled vitest
// runtime) calls a few built-ins that the oldest browsers we test don't have:
//
// Array/String/TypedArray.prototype.at Chromium 92, Firefox 90, Safari 15.4
// Object.hasOwn Chromium 93, Firefox 92, Safari 15.4
// Array.prototype.findLast(Index) Chromium 97, Firefox 104, Safari 15.4
//
// Without them the runtime throws before it can report back, and the run hangs
// until it times out (seen on Edge 89 and 90). Vitest 3 didn't depend on these, so the
// root setup doesn't need this.
//
// The polyfills go into both pages Vitest loads: the orchestrator page (via
// `browser.orchestratorScripts`, see legacyPolyfillsOrchestratorScript) and the
// test iframe (via this plugin's transformIndexHtml). They run before any other
// script, so the SDK code under test also sees them. The SDK source (lib/) doesn't use any of
// these today. If it starts to, tests on these old browsers would no longer
// catch it.
const POLYFILLS = `
(function () {
function define(target, name, value) {
if (target && !target[name]) {
Object.defineProperty(target, name, { value: value, writable: true, configurable: true });
}
}

function at(index) {
var length = this.length;
var i = Math.trunc(index) || 0;
if (i < 0) i += length;
return i < 0 || i >= length ? undefined : this[i];
}

function findLast(predicate, thisArg) {
for (var i = this.length - 1; i >= 0; i--) {
if (predicate.call(thisArg, this[i], i, this)) return this[i];
}
return undefined;
}

function findLastIndex(predicate, thisArg) {
for (var i = this.length - 1; i >= 0; i--) {
if (predicate.call(thisArg, this[i], i, this)) return i;
}
return -1;
}

var typedArrayPrototype = Object.getPrototypeOf(Int8Array.prototype);

define(Array.prototype, 'at', at);
define(String.prototype, 'at', at);
define(typedArrayPrototype, 'at', at);
define(Array.prototype, 'findLast', findLast);
define(Array.prototype, 'findLastIndex', findLastIndex);
define(typedArrayPrototype, 'findLast', findLast);
define(typedArrayPrototype, 'findLastIndex', findLastIndex);
define(Object, 'hasOwn', function (object, key) {
return Object.prototype.hasOwnProperty.call(Object(object), key);
});
})();
`;

// For `browser.orchestratorScripts`. The orchestrator page doesn't go through
// transformIndexHtml. A classic script (not a module) runs while the page is
// parsed, before Vitest's module scripts.
export const legacyPolyfillsOrchestratorScript = {
id: 'legacy-polyfills.js',
content: POLYFILLS,
type: 'text/javascript',
};

// Adds the polyfills to the test iframe page
export function legacyPolyfillsPlugin(): Plugin {
return {
name: 'legacy-polyfills',
transformIndexHtml() {
return [
{
tag: 'script',
children: POLYFILLS,
injectTo: 'head-prepend',
},
];
},
};
}
Loading
Loading