Skip to content
Open
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
26 changes: 26 additions & 0 deletions .github/workflows/javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ jobs:
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}

browser_tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 2
matrix:
browser: ['chrome', 'firefox', 'edge', 'safari']
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
USE_LOCAL_BROWSER: 'false'
TEST_BROWSER: ${{ matrix.browser }}
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 20
cache: 'npm'
cache-dependency-path: ./package-lock.json
- name: Browser tests - ${{ matrix.browser }}
working-directory: .
run: |
npm install
npm run test-browser

# crossbrowser_and_umd_unit_tests:
# runs-on: ubuntu-latest
# env:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ browserstack.err
local.log

**/*.gen.ts

.env
65 changes: 39 additions & 26 deletions lib/core/audience_evaluator/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { beforeEach, afterEach, describe, it, vi, expect, afterAll } from 'vitest';
import { beforeEach, afterEach, describe, it, vi, expect, afterAll, MockInstance } from 'vitest';

import AudienceEvaluator, { createAudienceEvaluator } from './index';
import * as conditionTreeEvaluator from '../condition_tree_evaluator';
Expand All @@ -23,6 +23,9 @@ import { getMockLogger } from '../../tests/mock/mock_logger';
import { Audience, OptimizelyDecideOption, OptimizelyDecision } from '../../shared_types';
import { IOptimizelyUserContext } from '../../optimizely_user_context';

vi.mock('../condition_tree_evaluator', { spy: true });
vi.mock('../custom_attribute_condition_evaluator', { spy: true });

let mockLogger = getMockLogger();

const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimizelyUserContext => ({
Expand Down Expand Up @@ -111,7 +114,7 @@ describe('lib/core/audience_evaluator', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

describe('APIs', () => {
Expand Down Expand Up @@ -207,20 +210,19 @@ describe('lib/core/audience_evaluator', () => {
});

describe('integration with dependencies', () => {
const evaluateSpy = conditionTreeEvaluator.evaluate as unknown as MockInstance<typeof conditionTreeEvaluator.evaluate>;
const getEvaluatorSpy = customAttributeConditionEvaluator.getEvaluator as unknown as MockInstance<typeof customAttributeConditionEvaluator.getEvaluator>;

beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.resetAllMocks();
});

afterAll(() => {
vi.resetAllMocks();
});

it('returns true if conditionTreeEvaluator.evaluate returns true', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(true);
evaluateSpy.mockReturnValue(true);
const result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
Expand All @@ -230,7 +232,7 @@ describe('lib/core/audience_evaluator', () => {
});

it('returns false if conditionTreeEvaluator.evaluate returns false', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(false);
evaluateSpy.mockReturnValue(false);
const result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
Expand All @@ -240,7 +242,7 @@ describe('lib/core/audience_evaluator', () => {
});

it('returns false if conditionTreeEvaluator.evaluate returns null', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(null);
evaluateSpy.mockReturnValue(null);
const result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
Expand All @@ -250,13 +252,13 @@ describe('lib/core/audience_evaluator', () => {
});

it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation((conditions: any, leafEvaluator) => {
evaluateSpy.mockImplementation((conditions: any, leafEvaluator: any) => {
return leafEvaluator(conditions[1]);
});

const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(false);

vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({
getEvaluatorSpy.mockReturnValue({
evaluate: mockCustomAttributeConditionEvaluator,
});

Expand All @@ -277,26 +279,28 @@ describe('lib/core/audience_evaluator', () => {
});

describe('Audience evaluation logging', () => {
let mockCustomAttributeConditionEvaluator: ReturnType<typeof vi.fn>;

const evaluateSpy = conditionTreeEvaluator.evaluate as unknown as MockInstance<typeof conditionTreeEvaluator.evaluate>;
const getEvaluatorSpy = customAttributeConditionEvaluator.getEvaluator as unknown as MockInstance<typeof customAttributeConditionEvaluator.getEvaluator>;

beforeEach(() => {
mockCustomAttributeConditionEvaluator = vi.fn();
vi.spyOn(conditionTreeEvaluator, 'evaluate');
vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({
evaluate: mockCustomAttributeConditionEvaluator,
});
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
afterAll(() => {
vi.resetAllMocks();
});

it('logs correctly when conditionTreeEvaluator.evaluate returns null', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => {
evaluateSpy.mockImplementationOnce((conditions: any, leafEvaluator) => {
return leafEvaluator(conditions[1]);
});

mockCustomAttributeConditionEvaluator.mockReturnValue(null);
const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(null);

getEvaluatorSpy.mockReturnValue({
evaluate: mockCustomAttributeConditionEvaluator,
});

const userAttributes = { device_model: 5.5 };
const user = getMockUserContext(userAttributes);

Expand All @@ -319,11 +323,15 @@ describe('lib/core/audience_evaluator', () => {
});

it('logs correctly when conditionTreeEvaluator.evaluate returns true', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => {
evaluateSpy.mockImplementationOnce((conditions: any, leafEvaluator) => {
return leafEvaluator(conditions[1]);
});

mockCustomAttributeConditionEvaluator.mockReturnValue(true);
const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(true);

getEvaluatorSpy.mockReturnValue({
evaluate: mockCustomAttributeConditionEvaluator,
});

const userAttributes = { device_model: 'iphone' };
const user = getMockUserContext(userAttributes);
Expand All @@ -345,11 +353,16 @@ describe('lib/core/audience_evaluator', () => {
});

it('logs correctly when conditionTreeEvaluator.evaluate returns false', () => {
vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => {
evaluateSpy.mockImplementationOnce((conditions: any, leafEvaluator) => {
return leafEvaluator(conditions[1]);
});

mockCustomAttributeConditionEvaluator.mockReturnValue(false);
const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(false);

getEvaluatorSpy.mockReturnValue({
evaluate: mockCustomAttributeConditionEvaluator,
});


const userAttributes = { device_model: 'android' };
const user = getMockUserContext(userAttributes);
Expand Down
41 changes: 27 additions & 14 deletions lib/core/bucketer/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, vi, afterEach, MockInstance } from 'vitest';
import { sprintf } from '../../utils/fns';
import projectConfig, { ProjectConfig } from '../../project_config/project_config';
import { getTestProjectConfig } from '../../tests/test_data';
import { INVALID_BUCKETING_ID, INVALID_GROUP_ID } from 'error_message';
import * as bucketer from './';
import * as bucketValueGenerator from './bucket_value_generator';

vi.mock('./bucket_value_generator', { spy: true });

import {
USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
Expand Down Expand Up @@ -65,8 +67,12 @@ describe('excluding groups', () => {
let configObj;
const mockLogger = getMockLogger();
let bucketerParams: BucketerParams;
const mockGenerateBucketValue = bucketValueGenerator.generateBucketValue as unknown as
MockInstance<typeof bucketValueGenerator.generateBucketValue>;

beforeEach(() => {
vi.clearAllMocks();

setLogSpy(mockLogger);
configObj = projectConfig.createProjectConfig(cloneDeep(testData));

Expand All @@ -83,13 +89,12 @@ describe('excluding groups', () => {
validateEntity: true,
};

vi.spyOn(bucketValueGenerator, 'generateBucketValue')
.mockReturnValueOnce(50)
mockGenerateBucketValue.mockReturnValueOnce(50)
.mockReturnValueOnce(50000);
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

it('should return decision response with correct variation ID when provided bucket value', async () => {
Expand All @@ -113,8 +118,12 @@ describe('including groups: random', () => {
let configObj: ProjectConfig;
const mockLogger = getMockLogger();
let bucketerParams: BucketerParams;
const mockGenerateBucketValue = bucketValueGenerator.generateBucketValue as unknown as
MockInstance<typeof bucketValueGenerator.generateBucketValue>;

beforeEach(() => {
vi.clearAllMocks();

setLogSpy(mockLogger);
configObj = projectConfig.createProjectConfig(cloneDeep(testData));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -133,11 +142,11 @@ describe('including groups: random', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

it('should return decision response with the proper variation for a user in a grouped experiment', () => {
vi.spyOn(bucketValueGenerator, 'generateBucketValue')
mockGenerateBucketValue
.mockReturnValueOnce(50)
.mockReturnValueOnce(50);

Expand All @@ -156,7 +165,7 @@ describe('including groups: random', () => {
});

it('should return decision response with variation null when a user is bucketed into a different grouped experiment than the one speicfied', () => {
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "speicfied" should be "specified".

Suggested change
it('should return decision response with variation null when a user is bucketed into a different grouped experiment than the one speicfied', () => {
it('should return decision response with variation null when a user is bucketed into a different grouped experiment than the one specified', () => {

Copilot uses AI. Check for mistakes.
vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValue(5000);
mockGenerateBucketValue.mockReturnValue(5000);

const decisionResponse = bucketer.bucket(bucketerParams);

Expand All @@ -173,7 +182,7 @@ describe('including groups: random', () => {
});

it('should return decision response with variation null when a user is not bucketed into any experiments in the random group', () => {
vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValue(50000);
mockGenerateBucketValue.mockReturnValue(50000);

const decisionResponse = bucketer.bucket(bucketerParams);

Expand All @@ -185,7 +194,7 @@ describe('including groups: random', () => {
});

it('should return decision response with variation null when a user is bucketed into traffic space of deleted experiment within a random group', () => {
vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(9000);
mockGenerateBucketValue.mockReturnValueOnce(9000);

const decisionResponse = bucketer.bucket(bucketerParams);

Expand Down Expand Up @@ -215,8 +224,12 @@ describe('including groups: overlapping', () => {
let configObj: ProjectConfig;
const mockLogger = getMockLogger();
let bucketerParams: BucketerParams;
const mockGenerateBucketValue = bucketValueGenerator.generateBucketValue as unknown as
MockInstance<typeof bucketValueGenerator.generateBucketValue>;

beforeEach(() => {
vi.clearAllMocks();

setLogSpy(mockLogger);
configObj = projectConfig.createProjectConfig(cloneDeep(testData));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -235,11 +248,11 @@ describe('including groups: overlapping', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

it('should return decision response with variation when a user falls into an experiment within an overlapping group', () => {
vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(0);
mockGenerateBucketValue.mockReturnValueOnce(0);

const decisionResponse = bucketer.bucket(bucketerParams);

Expand All @@ -249,7 +262,7 @@ describe('including groups: overlapping', () => {
});

it('should return decision response with variation null when a user does not fall into an experiment within an overlapping group', () => {
vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(3000);
mockGenerateBucketValue.mockReturnValueOnce(3000);
const decisionResponse = bucketer.bucket(bucketerParams);

expect(decisionResponse.result).toBeNull();
Expand Down Expand Up @@ -288,7 +301,7 @@ describe('bucket value falls into empty traffic allocation ranges', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

it('should return decision response with variation null', () => {
Expand Down Expand Up @@ -338,7 +351,7 @@ describe('traffic allocation has invalid variation ids', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.resetAllMocks();
});

it('should return decision response with variation null', () => {
Expand Down
Loading
Loading