feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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 { logger } from '@coze-arch/logger';
|
||||
|
||||
import { readFromCache, saveToCache } from '../src/utils/persist-cache'; // Adjust the import path
|
||||
|
||||
// Mocking localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
};
|
||||
|
||||
vi.stubGlobal('localStorage', localStorageMock);
|
||||
|
||||
describe('Feature Flags Cache', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('readFromCache', () => {
|
||||
it('should return undefined if cache is empty', async () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce(undefined);
|
||||
const result = await readFromCache();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return feature flags if cache has valid data', async () => {
|
||||
const validFlags = { feature1: true, feature2: false };
|
||||
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(validFlags));
|
||||
const result = await readFromCache();
|
||||
expect(result).toEqual(validFlags);
|
||||
});
|
||||
|
||||
it('should return undefined if cache has invalid data', async () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce(
|
||||
JSON.stringify({ invalid: 'data' }),
|
||||
);
|
||||
const result = await readFromCache();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveToCache', () => {
|
||||
it('should save feature flags to cache', async () => {
|
||||
const flags = { feature1: true, feature2: false };
|
||||
await saveToCache(flags);
|
||||
expect(localStorageMock.setItem).toBeCalledWith(
|
||||
'cache:@coze-arch/bot-flags',
|
||||
JSON.stringify(flags),
|
||||
);
|
||||
});
|
||||
|
||||
it('should save feature flags to cache', async () => {
|
||||
await saveToCache({ fg: 'test' });
|
||||
expect(localStorageMock.setItem).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should save feature flags to cache', async () => {
|
||||
localStorageMock.setItem.mockImplementation(() => {
|
||||
throw new Error('test');
|
||||
});
|
||||
await saveToCache({ feature: true });
|
||||
expect(logger.persist.error).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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 Mock } from 'vitest';
|
||||
|
||||
import { featureFlagStorage } from '../src/utils/storage';
|
||||
import {
|
||||
readFgPromiseFromContext,
|
||||
readFgValuesFromContext,
|
||||
} from '../src/utils/read-from-context';
|
||||
import { readFromCache, saveToCache } from '../src/utils/persist-cache';
|
||||
|
||||
const fetchFeatureGating = vi.fn();
|
||||
|
||||
const $wait = ms => new Promise(r => setTimeout(r, ms));
|
||||
vi.mock('../src/utils/wait', () => ({
|
||||
wait: vi.fn().mockImplementation($wait),
|
||||
ONE_SEC: 1000,
|
||||
nextTick: vi.fn().mockImplementation(async () => {
|
||||
await $wait(10);
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../src/utils/persist-cache', () => ({
|
||||
readFromCache: vi.fn(),
|
||||
saveToCache: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/utils/read-from-context', () => ({
|
||||
readFgPromiseFromContext: vi.fn(),
|
||||
readFgValuesFromContext: vi.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../src/utils/storage', () => ({
|
||||
featureFlagStorage: {
|
||||
setFlags: vi.fn(),
|
||||
getFlags: vi.fn().mockReturnValue({}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('pullFeatureFlags', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
// Runs successfully with default context
|
||||
it('should access values from global static value', async () => {
|
||||
(readFgValuesFromContext as Mock).mockReturnValue({ foo: true });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({ fetchFeatureGating });
|
||||
|
||||
expect(readFgValuesFromContext).toBeCalled();
|
||||
expect(readFgPromiseFromContext).not.toBeCalled();
|
||||
expect(readFromCache).not.toBeCalled();
|
||||
expect(fetchFeatureGating).not.toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
||||
expect(saveToCache).toBeCalledWith({ foo: true });
|
||||
});
|
||||
|
||||
// Runs successfully with default context
|
||||
it('should access values from global context', async () => {
|
||||
(readFgValuesFromContext as Mock).mockReturnValue(undefined);
|
||||
readFgPromiseFromContext.mockResolvedValue({ foo: true });
|
||||
readFromCache.mockResolvedValue(undefined);
|
||||
fetchFeatureGating.mockResolvedValue(undefined);
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({ fetchFeatureGating });
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
||||
expect(saveToCache).toBeCalledWith({ foo: true });
|
||||
});
|
||||
|
||||
it('should access values from localstorage', async () => {
|
||||
readFgPromiseFromContext.mockResolvedValueOnce(undefined);
|
||||
readFromCache.mockResolvedValue({ foo: true });
|
||||
fetchFeatureGating.mockResolvedValue(undefined);
|
||||
readFgPromiseFromContext.mockResolvedValueOnce({ foo: false });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
vi.useFakeTimers();
|
||||
const p = pullFeatureFlags({ pollingInterval: 1000, fetchFeatureGating });
|
||||
vi.runAllTimersAsync();
|
||||
await p;
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(fetchFeatureGating).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
||||
});
|
||||
|
||||
it('should access values from api', async () => {
|
||||
readFgPromiseFromContext.mockResolvedValue(undefined);
|
||||
readFromCache.mockResolvedValue(undefined);
|
||||
fetchFeatureGating.mockResolvedValue({ foo: true });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({ fetchFeatureGating });
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(fetchFeatureGating).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
||||
expect(saveToCache).toBeCalledWith({ foo: true });
|
||||
});
|
||||
|
||||
it('should access values from global context firstly', async () => {
|
||||
// 从localStorage & global context 都取到值的情况下,优先使用 context 值
|
||||
readFromCache.mockImplementation(async () => {
|
||||
await $wait(100);
|
||||
return { foo: true };
|
||||
});
|
||||
readFgPromiseFromContext.mockResolvedValue({ foo: false });
|
||||
fetchFeatureGating.mockResolvedValue(undefined);
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({ fetchFeatureGating });
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(fetchFeatureGating).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: false });
|
||||
});
|
||||
|
||||
it('should fallback to default value', async () => {
|
||||
readFromCache.mockResolvedValue(undefined);
|
||||
readFgPromiseFromContext.mockResolvedValue(undefined);
|
||||
fetchFeatureGating.mockResolvedValueOnce(undefined);
|
||||
|
||||
fetchFeatureGating.mockResolvedValueOnce({ foo: true });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({
|
||||
strict: false,
|
||||
pollingInterval: 10,
|
||||
timeout: 1000,
|
||||
fetchFeatureGating,
|
||||
});
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalledTimes(2);
|
||||
expect(readFromCache).toBeCalledTimes(2);
|
||||
expect(fetchFeatureGating).toBeCalledTimes(2);
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should throw error with strict mode', async () => {
|
||||
readFromCache.mockResolvedValue(undefined);
|
||||
readFgPromiseFromContext.mockResolvedValue(undefined);
|
||||
fetchFeatureGating.mockResolvedValue(undefined);
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await expect(
|
||||
pullFeatureFlags({
|
||||
strict: true,
|
||||
pollingInterval: 10,
|
||||
fetchFeatureGating,
|
||||
}),
|
||||
).rejects.toThrowError('Fetch Feature Flags timeout');
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(fetchFeatureGating).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw error with strict mode once timeout', async () => {
|
||||
const resolveAfterTimeout = async () => {
|
||||
await $wait(2000);
|
||||
return { foo: true };
|
||||
};
|
||||
readFromCache.mockImplementation(resolveAfterTimeout);
|
||||
readFgPromiseFromContext.mockImplementation(resolveAfterTimeout);
|
||||
fetchFeatureGating.mockImplementation(resolveAfterTimeout);
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await expect(
|
||||
pullFeatureFlags({
|
||||
strict: true,
|
||||
timeout: 1000,
|
||||
pollingInterval: 10,
|
||||
fetchFeatureGating,
|
||||
}),
|
||||
).rejects.toThrowError('Fetch Feature Flags timeout');
|
||||
|
||||
expect(readFgPromiseFromContext).toBeCalled();
|
||||
expect(readFromCache).toBeCalled();
|
||||
expect(fetchFeatureGating).toBeCalled();
|
||||
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should resolve value even if any step failure', async () => {
|
||||
readFromCache.mockResolvedValue({ foo: true });
|
||||
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
||||
readFgValuesFromContext.mockReturnValue(undefined);
|
||||
fetchFeatureGating.mockRejectedValueOnce(new Error('test'));
|
||||
fetchFeatureGating.mockResolvedValue({ foo: false });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
vi.useFakeTimers();
|
||||
const p = pullFeatureFlags({
|
||||
pollingInterval: 10,
|
||||
timeout: 10,
|
||||
fetchFeatureGating,
|
||||
});
|
||||
vi.runAllTimersAsync();
|
||||
await p;
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: true });
|
||||
expect(featureFlagStorage.setFlags).toBeCalledWith({ foo: false });
|
||||
});
|
||||
|
||||
it('should fallback to default value & and retry even if all step failure', async () => {
|
||||
readFromCache.mockRejectedValue(new Error('wfe'));
|
||||
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
||||
fetchFeatureGating.mockRejectedValueOnce(new Error('test'));
|
||||
|
||||
fetchFeatureGating.mockResolvedValue({ foo: true });
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await pullFeatureFlags({ pollingInterval: 10, fetchFeatureGating });
|
||||
|
||||
expect(fetchFeatureGating).toBeCalledTimes(2);
|
||||
expect(featureFlagStorage.setFlags.mock.calls[0][0]).toEqual({});
|
||||
expect(featureFlagStorage.setFlags.mock.calls[1][0]).toEqual({ foo: true });
|
||||
});
|
||||
|
||||
it('should throw Error if all step break down', async () => {
|
||||
(readFgValuesFromContext as Mock).mockImplementationOnce(() => {
|
||||
throw new Error('test');
|
||||
});
|
||||
readFromCache.mockRejectedValue(new Error('wfe'));
|
||||
readFgPromiseFromContext.mockRejectedValue(new Error('jweofj'));
|
||||
fetchFeatureGating.mockRejectedValue(new Error('test'));
|
||||
|
||||
const { pullFeatureFlags } = await import('../src/pull-feature-flags');
|
||||
|
||||
// Invoke function
|
||||
await expect(
|
||||
pullFeatureFlags({ strict: true, timeout: 1000, fetchFeatureGating }),
|
||||
).rejects.toThrowError('Fetch Feature Flags timeout');
|
||||
|
||||
expect(featureFlagStorage.setFlags).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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 {
|
||||
readFgPromiseFromContext,
|
||||
readFgValuesFromContext,
|
||||
} from '../src/utils/read-from-context';
|
||||
|
||||
describe('readFgPromiseFromContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return feature flags if set within timeout', async () => {
|
||||
const featureFlags = { feature1: true, feature2: false };
|
||||
vi.stubGlobal(
|
||||
'__fetch_fg_promise__',
|
||||
Promise.resolve({ data: featureFlags }),
|
||||
);
|
||||
|
||||
const result = await readFgPromiseFromContext();
|
||||
expect(result).toEqual(featureFlags);
|
||||
});
|
||||
|
||||
it('should return undefined if feature flags are not set', async () => {
|
||||
vi.stubGlobal('__fetch_fg_promise__', undefined);
|
||||
const result = await readFgPromiseFromContext();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('readFgValuesFromContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return feature flags if set within timeout', () => {
|
||||
const featureFlags = { feature1: true, feature2: false };
|
||||
vi.stubGlobal('__fg_values__', featureFlags);
|
||||
|
||||
const result = readFgValuesFromContext();
|
||||
expect(result).toEqual(featureFlags);
|
||||
});
|
||||
|
||||
it('should return undefined if feature flags are not set', async () => {
|
||||
vi.stubGlobal('__fg_values__', undefined);
|
||||
const result = await readFgValuesFromContext();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
125
frontend/packages/arch/bot-flags/__tests__/storage.test.ts
Normal file
125
frontend/packages/arch/bot-flags/__tests__/storage.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
describe('FeatureFlagStorage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('should initialize correctly', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
expect(featureFlagStorage.inited).toBe(false);
|
||||
});
|
||||
|
||||
it('should return inited status correctly', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
expect(featureFlagStorage.inited).toBe(false);
|
||||
featureFlagStorage.setFlags({ feature1: true });
|
||||
expect(featureFlagStorage.inited).toBe(true);
|
||||
});
|
||||
|
||||
it('should set and get feature flags correctly', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const flags = { feature1: true, feature2: false };
|
||||
featureFlagStorage.setFlags(flags);
|
||||
const fgValues = featureFlagStorage.getFlags();
|
||||
|
||||
expect(fgValues.feature1).toEqual(true);
|
||||
expect(fgValues.feature2).toEqual(false);
|
||||
// fallback to false if key not exits
|
||||
expect(fgValues.feature3).toEqual(false);
|
||||
});
|
||||
|
||||
it('should emit change event on setting new flags', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const flags = { feature1: true, feature2: false };
|
||||
const listener = vi.fn();
|
||||
featureFlagStorage.on('change', listener);
|
||||
|
||||
featureFlagStorage.setFlags(flags);
|
||||
expect(listener).toHaveBeenCalledWith(flags);
|
||||
});
|
||||
|
||||
it('should not emit change event when setting same flags', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const flags = { feature1: true, feature2: false };
|
||||
featureFlagStorage.setFlags(flags);
|
||||
|
||||
const listener = vi.fn();
|
||||
featureFlagStorage.on('change', listener);
|
||||
|
||||
featureFlagStorage.setFlags(flags);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when getting flags before initialization', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
expect(() => featureFlagStorage.getFlags()).toThrow(
|
||||
'Trying access feature flag values before the storage been init.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear flags correctly', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
featureFlagStorage.setFlags({ feature1: true });
|
||||
featureFlagStorage.clear();
|
||||
expect(() => featureFlagStorage.getFlags()).toThrow();
|
||||
});
|
||||
|
||||
it('should return all keys', async () => {
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
featureFlagStorage.setFlags({ feature1: true, feature2: false });
|
||||
const flags = featureFlagStorage.getFlags();
|
||||
expect(flags.keys).toEqual(['feature1', 'feature2']);
|
||||
});
|
||||
|
||||
it('should return none keys', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const flags = featureFlagStorage.getFlags();
|
||||
expect(flags.keys).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return isInited', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const flags = featureFlagStorage.getFlags();
|
||||
expect(flags.isInited).toEqual(false);
|
||||
featureFlagStorage.setFlags({ feature1: true, feature2: false });
|
||||
expect(flags.isInited).toEqual(true);
|
||||
});
|
||||
|
||||
it('should unshift the function into interceptors by calling `use`', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
const { featureFlagStorage } = await import('../src/utils/storage');
|
||||
const fnGetter = vi.fn();
|
||||
featureFlagStorage.use(fnGetter);
|
||||
const flags = featureFlagStorage.getFlags();
|
||||
const testKey = 'bot.arch.bot.fg.test.1';
|
||||
// trigger get of the proxy
|
||||
flags[testKey];
|
||||
expect(fnGetter).toHaveBeenCalledWith(testKey);
|
||||
|
||||
// trigger set of the proxy
|
||||
const fnSetter = vi.fn(() => {
|
||||
flags['bot.arch.bot.fg.test.1'] = true;
|
||||
});
|
||||
expect(fnSetter).toThrowError();
|
||||
});
|
||||
});
|
||||
71
frontend/packages/arch/bot-flags/__tests__/use-flags.test.ts
Normal file
71
frontend/packages/arch/bot-flags/__tests__/use-flags.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { featureFlagStorage } from '../src/utils/storage';
|
||||
import { useFlags } from '../src/use-flags'; // Adjust the import path
|
||||
import { getFlags } from '../src/get-flags';
|
||||
|
||||
vi.mock('../src/utils/storage', () => ({
|
||||
featureFlagStorage: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../src/get-flags', () => ({
|
||||
getFlags: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useFlags', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return initial flags', () => {
|
||||
const initialFlags = { feature1: true, feature2: false };
|
||||
getFlags.mockImplementation(() => initialFlags);
|
||||
|
||||
const { result } = renderHook(() => useFlags());
|
||||
|
||||
expect(result.current[0]).toEqual(initialFlags);
|
||||
});
|
||||
|
||||
it('should update flags on storage change', () => {
|
||||
const initialFlags = { feature1: true, feature2: false };
|
||||
const updatedFlags = { feature1: false, feature2: true };
|
||||
getFlags.mockImplementation(() => initialFlags);
|
||||
|
||||
const { result } = renderHook(() => useFlags());
|
||||
|
||||
act(() => {
|
||||
getFlags.mockImplementation(() => updatedFlags);
|
||||
featureFlagStorage.on.mock.calls[0][1](); // Simulate 'change' event
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual(updatedFlags);
|
||||
});
|
||||
|
||||
it('should remove event listener on unmount', () => {
|
||||
const { unmount } = renderHook(() => useFlags());
|
||||
|
||||
unmount();
|
||||
|
||||
expect(featureFlagStorage.off).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user