fix(ui): some ui reviews and fixes for feature flags (#1355)

* fix(ui): some ui reviews and fixes for feature flags

* feat(ui): added some tests

* feat(ui): added some tests
This commit is contained in:
Delirium 2023-06-22 10:27:11 +02:00 committed by GitHub
parent c8f391c771
commit 15e2744acb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1033 additions and 453 deletions

View file

@ -104,6 +104,12 @@ jobs:
run: |
cd frontend
yarn test
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: ui
name: ui
- name: Run Frontend
run: |
cd frontend

View file

@ -10,10 +10,15 @@ function FFlagItem({ flag }: { flag: FeatureFlag }) {
const { featureFlagsStore, userStore } = useStore();
const toggleActivity = () => {
flag.setIsEnabled(!flag.isActive);
const newValue = !flag.isActive
flag.setIsEnabled(newValue);
featureFlagsStore.updateFlag(flag, true).then(() => {
toast.success('Feature flag updated.');
})
.catch(() => {
flag.setIsEnabled(!newValue);
toast.error('Failed to update flag.')
})
}
const flagIcon = flag.isSingleOption ? 'fflag-single' : 'fflag-multi' as const

View file

@ -5,6 +5,7 @@ import FilterList from 'Shared/Filters/FilterList';
import { nonFlagFilters } from 'Types/filter/newFilter';
import { observer } from 'mobx-react-lite';
import { Conditions } from "App/mstore/types/FeatureFlag";
import FilterSelection from 'Shared/Filters/FilterSelection';
interface Props {
set: number;
@ -15,8 +16,8 @@ interface Props {
function RolloutCondition({ set, conditions, removeCondition, index }: Props) {
const [forceRender, forceRerender] = React.useState(false);
const onAddFilter = () => {
conditions.filter.addFilter({});
const onAddFilter = (filter = {}) => {
conditions.filter.addFilter(filter);
forceRerender(!forceRender);
};
const onUpdateFilter = (filterIndex: number, filter: any) => {
@ -48,7 +49,7 @@ function RolloutCondition({ set, conditions, removeCondition, index }: Props) {
<div className={'p-2 rounded bg-gray-lightest'}>Set {set}</div>
<div
className={cn(
'p-2 cursor-pointer rounded ml-auto',
'p-2 px-4 cursor-pointer rounded ml-auto',
'hover:bg-teal-light'
)}
onClick={() => removeCondition(index)}
@ -63,14 +64,17 @@ function RolloutCondition({ set, conditions, removeCondition, index }: Props) {
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
supportsEmpty
hideEventsOrder
excludeFilterKeys={nonFlagFilters}
/>
</div>
<Button variant={'text-primary'} onClick={() => onAddFilter()}>
+ Add Condition
</Button>
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
excludeFilterKeys={nonFlagFilters}
>
<Button variant="text-primary" icon="plus">Add Condition</Button>
</FilterSelection>
</div>
<div className={'px-4 py-2 flex items-center gap-2'}>
<span>Rollout to</span>

View file

@ -12,7 +12,7 @@ function Header({ current, onCancel, onSave, isNew }: any) {
<div className={'flex items-center gap-2'}>
<Button variant="text-primary" onClick={onCancel}>
{isNew ? "Cancel" : "Back"}
Cancel
</Button>
<Button variant="primary" onClick={onSave}>
Save

View file

@ -5,6 +5,35 @@ import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import cn from 'classnames';
const alphabet = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
function Multivariant() {
const { featureFlagsStore } = useStore();
@ -14,17 +43,23 @@ function Multivariant() {
return (
<div>
<div className={'text-sm text-disabled-text mt-1 flex items-center gap-1'}>
Users who meet release conditions will be server variant's
<code className={'p-1 text-red rounded bg-gray-lightest'}>key</code> based on specific
distribution.
</div>
<div className={'flex items-center gap-2 font-semibold mt-4'}>
<div style={{ flex: 1 }}>Variant</div>
<div style={{ flex: 3 }}>Key</div>
<div style={{ flex: 3 }}>Description</div>
<div style={{ flex: 3 }}>
<div style={{ flex: 4 }}>Key</div>
<div style={{ flex: 4 }}>Description</div>
<div style={{ flex: 4 }}>
<Payload />
</div>
<div style={{ flex: 3 }} className={'flex items-center gap-2'}>
<Rollout />{' '}
<div style={{ flex: 4 }} className={'flex items-center'}>
<Rollout />
<Button
variant={'text-primary'}
className={'font-normal ml-auto'}
onClick={featureFlagsStore.currentFflag!.redistributeVariants}
>
Distribute Equally
@ -33,79 +68,82 @@ function Multivariant() {
</div>
<div>
{featureFlagsStore.currentFflag!.variants.map((variant, ind) => {
console.log(variant, featureFlagsStore.currentFflag)
return (
<div className={'flex items-center gap-2 my-2 '} key={variant.index}>
<div style={{ flex: 1 }}>
<div className={'p-2 text-center bg-gray-lightest rounded-full w-10 h-10'}>
{ind + 1}
<div className={'flex items-center gap-2 my-2 '} key={variant.index}>
<div style={{ flex: 1 }}>
<div className={'p-2 text-center bg-gray-lightest rounded-full w-10 h-10'}>
{alphabet[ind] || ind + 1}
</div>
</div>
</div>
<div style={{ flex: 4 }}>
<Input
placeholder={'buy-btn-variant-1'}
value={variant.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setKey(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }}>
<Input
placeholder={'Very red button'}
value={variant.description}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setDescription(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }}>
<Input
placeholder={"Example: very important button, {'buttonColor': 'red'}"}
value={variant.payload}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setPayload(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }} className={'flex items-center gap-2'}>
<Input
className={
featureFlagsStore.currentFflag!.isRedDistribution
? '!border-red !text-red !w-full'
: '!w-full'
}
type={'tel'}
placeholder={avg}
value={variant.rolloutPercentage}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setRollout(parseInt(e.target.value.replace(/\D/g, ''), 10))
}
/>
<div
className={cn(
'p-2 cursor-pointer rounded ml-auto',
featureFlagsStore.currentFflag!.variants.length === 1
? 'cursor-not-allowed'
: 'hover:bg-teal-light'
)}
onClick={() =>
featureFlagsStore.currentFflag!.variants.length === 1
? null
: featureFlagsStore.currentFflag!.removeVariant(variant.index)
}
>
<Icon
name={'trash'}
color={featureFlagsStore.currentFflag!.variants.length === 1 ? '' : 'main'}
<div style={{ flex: 4 }}>
<Input
placeholder={'buy-btn-variant-1'}
value={variant.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setKey(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }}>
<Input
placeholder={'Very red button'}
value={variant.description}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setDescription(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }}>
<Input
placeholder={"Example: very important button, {'buttonColor': 'red'}"}
value={variant.payload}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setPayload(e.target.value)
}
/>
</div>
<div style={{ flex: 4 }} className={'flex items-center gap-2'}>
<Input
className={'!flex-1'}
type={'tel'}
wrapperClassName={'flex-1'}
placeholder={avg}
value={variant.rolloutPercentage}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
variant.setRollout(parseInt(e.target.value.replace(/\D/g, ''), 10))
}
/>
<div
className={cn(
'p-2 cursor-pointer rounded',
featureFlagsStore.currentFflag!.variants.length === 1
? 'cursor-not-allowed'
: 'hover:bg-teal-light'
)}
onClick={() =>
featureFlagsStore.currentFflag!.variants.length === 1
? null
: featureFlagsStore.currentFflag!.removeVariant(variant.index)
}
>
<Icon
name={'trash'}
color={featureFlagsStore.currentFflag!.variants.length === 1 ? '' : 'main'}
/>
</div>
</div>
</div>
</div>)})}
);
})}
</div>
<div className={'mt-2 flex justify-between w-full pr-4'}>
<Button variant={'text-primary'} onClick={featureFlagsStore.currentFflag!.addVariant}>
+ Add Variant
</Button>
{featureFlagsStore.currentFflag!.isRedDistribution ? (
<div className={'text-red'}>Total distribution is less than 100%</div>
) : null}
</div>
<Button variant={'text-primary'} onClick={featureFlagsStore.currentFflag!.addVariant}>
+ Add Variant
</Button>
</div>
);
}

View file

@ -110,7 +110,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
in your code.
</div>
<div className={'mt-4'}>
<div className={'mt-6'}>
<Description
current={current}
isDescrEditing={featureFlagsStore.isDescrEditing}
@ -119,7 +119,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
/>
</div>
<div className={'mt-4'}>
<div className={'mt-6'}>
<label className={'font-semibold'}>Feature Type</label>
<div style={{ width: 340 }}>
<SegmentSelection
@ -144,7 +144,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
<code className={'p-1 text-red rounded bg-gray-lightest'}>true</code> if they match
one or more rollout conditions.
</div>
<div>
<div className={"mt-6"}>
<Payload />
<Input placeholder={"Example: very important button, {'buttonColor': 'red'}"} className={'mt-2'} />
</div>
@ -154,7 +154,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
)}
</div>
<div className={'mt-4'}>
<div className={'mt-6'}>
<label className={'font-semibold'}>Persist flag across authentication</label>
<Toggler
checked={current.isPersist}
@ -169,7 +169,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
</div>
</div>
<div className={'mt-4'}>
<div className={'mt-6'}>
<label className={'font-semibold'}>Enable this feature flag (Status)?</label>
<Toggler
checked={current.isActive}
@ -181,11 +181,11 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
/>
</div>
<div className={'mt-4 p-4 rounded bg-gray-lightest'}>
<div className={'mt-6 p-4 rounded bg-gray-lightest'}>
<label className={'font-semibold'}>Rollout Conditions</label>
{current.conditions.length === 0 ? null
: (
<div className={'text-sm text-disabled-text'}>
<div className={'text-sm text-disabled-text mb-2'}>
Indicate the users for whom you intend to make this flag available. Keep in mind that
each set of conditions will be deployed separately from one another.
</div>

View file

@ -18,9 +18,11 @@ export default class FeatureFlagsStore {
sort = { order: 'DESC', query: '' };
page: number = 1;
readonly pageSize: number = 10;
client: typeof fflagsService
constructor() {
constructor(customClient?: typeof fflagsService) {
makeAutoObservable(this);
this.client = customClient ?? fflagsService
}
setFlagsSearch = (search: string) => {
@ -83,7 +85,7 @@ export default class FeatureFlagsStore {
isActive: this.activity === '0' ? undefined : this.activity === '1',
// userId: 3,
}
const { list } = await fflagsService.fetchFlags(filters);
const { list } = await this.client.fetchFlags(filters);
const flags = list.map((record) => new FeatureFlag(record));
this.setList(flags);
} catch (e) {
@ -98,8 +100,11 @@ export default class FeatureFlagsStore {
if (this.currentFflag.flagKey === '') {
return 'Feature flag must have a key'
}
if (this.currentFflag?.variants.findIndex((v) => v.value === '') !== -1) {
return 'Variants must include key'
if (!this.currentFflag.isSingleOption && this.currentFflag?.variants.findIndex((v) => v.value === '') !== -1) {
return 'All variants must include unique key'
}
if (this.currentFflag?.isRedDistribution) {
return 'Variants rollout percentage must add up to 100%'
}
return null;
}
@ -109,7 +114,7 @@ export default class FeatureFlagsStore {
this.setLoading(true);
try {
// @ts-ignore
const result = await fflagsService.createFlag(this.currentFflag.toJS());
const result = await this.client.createFlag(this.currentFflag.toJS());
this.addFlag(new FeatureFlag(result));
} catch (e) {
console.error(e);
@ -128,7 +133,7 @@ export default class FeatureFlagsStore {
}
try {
// @ts-ignore
const result = await fflagsService.updateFlag(usedFlag.toJS());
const result = await this.client.updateFlag(usedFlag.toJS());
if (!flag) this.setCurrentFlag(new FeatureFlag(result));
} catch (e) {
console.error('getting api error', e);
@ -142,7 +147,7 @@ export default class FeatureFlagsStore {
deleteFlag = async (id: FeatureFlag['featureFlagId']) => {
this.setLoading(true);
try {
await fflagsService.deleteFlag(id);
await this.client.deleteFlag(id);
this.removeFromList(id);
} catch (e) {
console.error(e);
@ -154,7 +159,7 @@ export default class FeatureFlagsStore {
fetchFlag = async (id: FeatureFlag['featureFlagId']) => {
this.setLoading(true);
try {
const result = await fflagsService.getFlag(id);
const result = await this.client.getFlag(id);
this.setCurrentFlag(new FeatureFlag(result));
} catch (e) {
console.error(e);

View file

@ -49,9 +49,9 @@ export class Variant {
rolloutPercentage: number = 100;
constructor(index: number, data?: Record<string, any>) {
makeAutoObservable(this)
Object.assign(this, data)
this.index = index;
makeAutoObservable(this)
}
setIndex = (index: number) => {
@ -145,7 +145,7 @@ export default class FeatureFlag {
isPersist: this.isPersist,
flagType: this.isSingleOption ? 'single' as const : 'multi' as const,
featureFlagId: this.featureFlagId,
variants: this.isSingleOption ? undefined : this.variants.map(v => ({ value: v.value, description: v.description, payload: v.payload, rolloutPercentage: v.rolloutPercentage })),
variants: this.isSingleOption ? undefined : this.variants?.map(v => ({ value: v.value, description: v.description, payload: v.payload, rolloutPercentage: v.rolloutPercentage })),
}
}

View file

@ -381,6 +381,7 @@ export default class Session {
addNotes(sessionNotes: Note[]) {
sessionNotes.forEach((note) => {
// @ts-ignore veri dirti
note.time = note.timestamp
})
// @ts-ignore

View file

@ -2,7 +2,7 @@
module.exports = {
preset: 'ts-jest',
rootDir: './',
testEnvironment: 'node',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^Types/session/(.+)$': '<rootDir>/app/types/session/$1',
'^App/(.+)$': '<rootDir>/app/$1',

View file

@ -36,6 +36,7 @@
"html-to-image": "^1.9.0",
"html2canvas": "^1.4.1",
"immutable": "^4.0.0-rc.12",
"jest-environment-jsdom": "^29.5.0",
"jsbi": "^4.1.0",
"jshint": "^2.11.1",
"jspdf": "^2.5.1",

View file

@ -0,0 +1,96 @@
import FeatureFlag, { Conditions, Variant } from '../app/mstore/types/FeatureFlag';
import { jest, test, expect, describe } from '@jest/globals';
jest.mock('App/mstore/types/filter', () => {
let filterData = { filters: [] }
class MockFilter {
ID_KEY = "filterId"
filterId = ''
name = ''
filters = []
eventsOrder = 'then'
eventsOrderSupport = ['then', 'or', 'and']
startTimestamp = 0
endTimestamp = 0
fromJson(json) {
this.name = json.name
this.filters = json.filters.map((i) => i)
this.eventsOrder = json.eventsOrder
return this
}
}
return MockFilter
})
describe('Feature flag type test', () => {
// Test cases for Conditions class
test('Conditions class methods work correctly', () => {
const conditions = new Conditions();
conditions.setRollout(50);
expect(conditions.rolloutPercentage).toBe(50);
const jsObject = conditions.toJS();
expect(jsObject.rolloutPercentage).toBe(50);
});
// Test cases for Variant class
test('Variant class methods work correctly', () => {
const variant = new Variant(1);
variant.setIndex(2);
expect(variant.index).toBe(2);
variant.setKey('key');
expect(variant.value).toBe('key');
variant.setDescription('description');
expect(variant.description).toBe('description');
variant.setPayload('payload');
expect(variant.payload).toBe('payload');
variant.setRollout(90);
expect(variant.rolloutPercentage).toBe(90);
});
// Test cases for FeatureFlag class
test('FeatureFlag class methods work correctly', () => {
const featureFlag = new FeatureFlag();
featureFlag.setPayload('payload');
expect(featureFlag.payload).toBe('payload');
featureFlag.addVariant();
expect(featureFlag.variants.length).toBe(2);
featureFlag.removeVariant(1);
expect(featureFlag.variants.length).toBe(1);
featureFlag.redistributeVariants();
expect(featureFlag.variants[0].rolloutPercentage).toBe(100);
featureFlag.addCondition();
expect(featureFlag.conditions.length).toBe(2);
featureFlag.removeCondition(1);
expect(featureFlag.conditions.length).toBe(1);
featureFlag.setFlagKey('flagKey');
expect(featureFlag.flagKey).toBe('flagKey');
featureFlag.setDescription('description');
expect(featureFlag.description).toBe('description');
featureFlag.setIsPersist(true);
expect(featureFlag.isPersist).toBe(true);
featureFlag.setIsSingleOption(true);
expect(featureFlag.isSingleOption).toBe(true);
featureFlag.setIsEnabled(true);
expect(featureFlag.isActive).toBe(true);
});
});

View file

@ -0,0 +1,90 @@
import { jest, test, expect, describe } from '@jest/globals';
import FeatureFlag from 'App/mstore/types/FeatureFlag';
import FeatureFlagsStore from 'App/mstore/FeatureFlagsStore';
const mockFflagsService = {
fetchFlags: jest.fn(),
createFlag: jest.fn(),
updateFlag: jest.fn(),
deleteFlag: jest.fn(),
getFlag: jest.fn(),
};
// not working
jest.mock('App/services', () => {
return {
fflagsService: mockFflagsService,
};
});
// working fine?
jest.mock('App/mstore/types/FeatureFlag', () => {
class FakeClass {
constructor(data) {
Object.assign(this, data);
}
toJS() {
return jest.fn(() => this)
}
}
return FakeClass;
})
describe('FeatureFlagsStore', () => {
test('should fetch flags', async () => {
const mockFlags = [{ featureFlagId: 1 }, { featureFlagId: 2 }];
mockFflagsService.fetchFlags.mockResolvedValueOnce({ list: mockFlags });
const store = new FeatureFlagsStore(mockFflagsService);
await store.fetchFlags();
expect(store.flags.length).toBe(mockFlags.length);
expect(store.flags[0].featureFlagId).toBe(mockFlags[0].featureFlagId);
expect(store.flags[1].featureFlagId).toBe(mockFlags[1].featureFlagId);
});
test('should create a flag', async () => {
const mockFlag = { featureFlagId: 3 };
mockFflagsService.createFlag.mockResolvedValueOnce(mockFlag);
const store = new FeatureFlagsStore(mockFflagsService);
store.currentFflag = new FeatureFlag();
await store.createFlag();
expect(store.flags.length).toBe(1);
expect(store.flags[0].featureFlagId).toBe(mockFlag.featureFlagId);
});
test('should update a flag', async () => {
const mockFlag = { featureFlagId: 4 };
mockFflagsService.updateFlag.mockResolvedValueOnce(mockFlag);
const store = new FeatureFlagsStore(mockFflagsService);
store.currentFflag = new FeatureFlag();
await store.updateFlag();
expect(store.currentFflag.featureFlagId).toBe(mockFlag.featureFlagId);
});
test('should delete a flag', async () => {
const mockFlagId = 5;
mockFflagsService.deleteFlag.mockResolvedValueOnce();
const store = new FeatureFlagsStore(mockFflagsService);
store.flags = [new FeatureFlag({ featureFlagId: mockFlagId })];
await store.deleteFlag(mockFlagId);
expect(store.flags.length).toBe(0);
});
test('should fetch a flag', async () => {
const mockFlag = { featureFlagId: 6 };
mockFflagsService.getFlag.mockResolvedValueOnce(mockFlag);
const store = new FeatureFlagsStore(mockFflagsService);
await store.fetchFlag(mockFlag.featureFlagId);
expect(store.currentFflag.featureFlagId).toBe(mockFlag.featureFlagId);
});
});

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,12 @@ import AttributeSender from '../main/modules/attributeSender.js'
import { describe, expect, test, jest, beforeEach, afterEach } from '@jest/globals'
describe('AttributeSender', () => {
let attributeSender
let appMock
let attributeSender: AttributeSender
let appMock: any
beforeEach(() => {
appMock = {
send: (...args) => args,
send: (...args: any[]) => args,
}
attributeSender = new AttributeSender(appMock)
})
@ -22,6 +22,7 @@ describe('AttributeSender', () => {
const id = 1
const name = 'color'
const value = 'red'
// @ts-ignore
const expectedMessage = [Type.SetNodeAttributeDict, id, 1, 2]
attributeSender.sendSetAttribute(id, name, value)
@ -39,6 +40,7 @@ describe('AttributeSender', () => {
expect(sendSpy).toHaveBeenCalledWith(
expect.arrayContaining([
// @ts-ignore
Type.SetNodeAttributeDict,
id,
expect.any(Number),
@ -55,6 +57,7 @@ describe('AttributeSender', () => {
attributeSender.sendSetAttribute(id, name, value)
// @ts-ignore
expect(sendSpy).toHaveBeenCalledWith([Type.StringDict, expect.any(Number), name])
})
@ -70,6 +73,7 @@ describe('AttributeSender', () => {
// 2 attributes + 1 stringDict name + 1 stringDict value
expect(sendSpy).toHaveBeenCalledTimes(4)
expect(sendSpy).toHaveBeenCalledWith(
// @ts-ignore
expect.not.arrayContaining([Type.StringDict, expect.any(Number), name]),
)
})
@ -87,6 +91,7 @@ describe('AttributeSender', () => {
// (attribute + stringDict name + stringDict value) * 2 = 6
expect(sendSpy).toHaveBeenCalledTimes(6)
expect(sendSpy).toHaveBeenCalledWith(
// @ts-ignore
expect.arrayContaining([Type.StringDict, expect.any(Number), name]),
)
})

View file

@ -0,0 +1,185 @@
import { jest, test, describe, beforeEach, afterEach, expect } from '@jest/globals'
import Session from '../main/app/Session'
import App from '../main/app/index.js'
import { generateRandomId } from '../main/utils.js'
jest.mock('../main/app/index.js') // Mock the App class
jest.mock('../main/utils.js') // Mock the generateRandomId function
describe('Session', () => {
let session: any
let mockApp
let mockSessionStorage: any
let mockOptions: any
beforeEach(() => {
mockSessionStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
}
mockApp = {
sessionStorage: mockSessionStorage,
options: {
ingestPoint: 'test',
},
}
mockApp.sessionStorage = mockSessionStorage
mockOptions = {
session_token_key: 'token_key',
session_pageno_key: 'pageno_key',
session_tabid_key: 'tabid_key',
}
// @ts-ignore
generateRandomId.mockReturnValue('random_id')
session = new Session(mockApp as unknown as App, mockOptions)
})
afterEach(() => {
jest.clearAllMocks()
})
test('creates a new Session with default values', () => {
expect(session).toBeDefined()
expect(session.getInfo()).toEqual({
sessionID: undefined,
metadata: {},
userID: null,
timestamp: 0,
projectID: undefined,
})
})
test('assigns new info correctly', () => {
const newInfo = {
sessionID: 'new_id',
metadata: { key: 'value' },
userID: 'user_1',
timestamp: 12345,
projectID: 'project_1',
}
session.assign(newInfo)
expect(session.getInfo()).toEqual(newInfo)
})
// Test for attachUpdateCallback
test('attaches an update callback correctly', () => {
const callback = jest.fn()
session.attachUpdateCallback(callback)
expect(session['callbacks']).toContain(callback)
})
// Test for handleUpdate
test('handles update correctly', () => {
const newInfo = { userID: 'user_2' }
const callback = jest.fn()
session.attachUpdateCallback(callback)
session['handleUpdate'](newInfo)
expect(callback).toHaveBeenCalledWith(newInfo)
})
// Test for setMetadata
test('sets metadata correctly', () => {
session.setMetadata('key', 'value')
expect(session['metadata']).toEqual({ key: 'value' })
})
// Test for setUserID
test('sets userID correctly', () => {
session.setUserID('user_1')
expect(session['userID']).toEqual('user_1')
})
// Test for setUserInfo
test('sets user info correctly', () => {
const userInfo = {
userBrowser: 'Chrome',
userCity: 'San Francisco',
userCountry: 'USA',
userDevice: 'Desktop',
userOS: 'Windows',
userState: 'CA',
}
session.setUserInfo(userInfo)
expect(session.userInfo).toEqual(userInfo)
})
// Test for getPageNumber
test('gets page number correctly', () => {
mockSessionStorage.getItem.mockReturnValue('2')
const pageNo = session.getPageNumber()
expect(pageNo).toEqual(2)
})
// Test for incPageNo
test('increments page number correctly', () => {
mockSessionStorage.getItem.mockReturnValue('2')
const pageNo = session.incPageNo()
expect(pageNo).toEqual(3)
})
// Test for getSessionToken
test('gets session token correctly', () => {
mockSessionStorage.getItem.mockReturnValue('token_1')
const token = session.getSessionToken()
expect(token).toEqual('token_1')
})
// Test for setSessionToken
test('sets session token correctly', () => {
session.setSessionToken('token_1')
expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
mockOptions.session_token_key,
'token_1',
)
})
// Test for applySessionHash
test('applies session hash correctly', () => {
const hash = '1&token_1'
session.applySessionHash(hash)
expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
mockOptions.session_token_key,
'token_1',
)
expect(mockSessionStorage.setItem).toHaveBeenCalledWith(mockOptions.session_pageno_key, '1')
})
// Test for getSessionHash
test('gets session hash correctly', () => {
mockSessionStorage.getItem.mockReturnValueOnce('1').mockReturnValueOnce('token_1')
const hash = session.getSessionHash()
expect(hash).toEqual('1&token_1')
})
// Test for getTabId
test('gets tabId correctly', () => {
expect(session.getTabId()).toEqual('random_id')
})
// Test for createTabId
test('creates tabId correctly', () => {
mockSessionStorage.getItem.mockReturnValueOnce(null).mockReturnValueOnce('random_id')
session['createTabId']()
expect(session.getTabId()).toEqual('random_id')
expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
mockOptions.session_tabid_key,
'random_id',
)
})
// Test for reset
test('resets session correctly', () => {
session.reset()
expect(session.getInfo()).toEqual({
sessionID: undefined,
metadata: {},
userID: null,
timestamp: 0,
projectID: undefined,
})
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(mockOptions.session_token_key)
})
})