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
94 changes: 94 additions & 0 deletions tests/mocks/ConfigCatClientMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { ClientCacheState, HookEvents, IConfig, IConfigCatClient, IConfigCatClientSnapshot, IEvaluationDetails, RefreshResult, SettingKeyValue, SettingTypeOf, SettingValue, User } from "../../src";

export class ConfigCatClientMockBase implements IConfigCatClient {
constructor(
public isOffline = false) {
}

getValueAsync<T extends SettingValue>(key: string, defaultValue: T, user?: User | undefined): Promise<SettingTypeOf<T>> {
throw new Error("Method not implemented.");
}
getValueDetailsAsync<T extends SettingValue>(key: string, defaultValue: T, user?: User | undefined): Promise<IEvaluationDetails<SettingTypeOf<T>>> {
throw new Error("Method not implemented.");
}
getAllKeysAsync(): Promise<string[]> {
throw new Error("Method not implemented.");
}
getAllValuesAsync(user?: User | undefined): Promise<SettingKeyValue<SettingValue>[]> {
throw new Error("Method not implemented.");
}
getAllValueDetailsAsync(user?: User | undefined): Promise<IEvaluationDetails<SettingValue>[]> {
throw new Error("Method not implemented.");
}
getKeyAndValueAsync(variationId: string): Promise<SettingKeyValue<SettingValue> | null> {
throw new Error("Method not implemented.");
}
forceRefreshAsync(): Promise<RefreshResult> {
throw new Error("Method not implemented.");
}
waitForReady(): Promise<ClientCacheState> {
throw new Error("Method not implemented.");
}
snapshot(): IConfigCatClientSnapshot {
throw new Error("Method not implemented.");
}
setDefaultUser(defaultUser: User): void {
throw new Error("Method not implemented.");
}
clearDefaultUser(): void {
throw new Error("Method not implemented.");
}
setOnline(): void {
throw new Error("Method not implemented.");
}
setOffline(): void {
throw new Error("Method not implemented.");
}
dispose(): void {
throw new Error("Method not implemented.");
}
addListener<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
throw new Error("Method not implemented.");
}
on<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
throw new Error("Method not implemented.");
}
once<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
throw new Error("Method not implemented.");
}
removeListener<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
throw new Error("Method not implemented.");
}
off<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
throw new Error("Method not implemented.");
}
removeAllListeners(eventName?: keyof HookEvents | undefined): this {
throw new Error("Method not implemented.");
}
listeners(eventName: keyof HookEvents): Function[] {
throw new Error("Method not implemented.");
}
listenerCount(eventName: keyof HookEvents): number {
throw new Error("Method not implemented.");
}
eventNames(): (keyof HookEvents)[] {
throw new Error("Method not implemented.");
}
}

export class ConfigCatClientSnapshotMockBase implements IConfigCatClientSnapshot {
constructor(
public cacheState = ClientCacheState.NoFlagData,
public fetchedConfig: IConfig | null = null) {
}

getAllKeys(): readonly string[] {
throw new Error("Method not implemented.");
}
getValue<T extends SettingValue>(key: string, defaultValue: T, user?: User | undefined): SettingTypeOf<T> {
throw new Error("Method not implemented.");
}
getValueDetails<T extends SettingValue>(key: string, defaultValue: T, user?: User | undefined): IEvaluationDetails<SettingTypeOf<T>> {
throw new Error("Method not implemented.");
}
}
16 changes: 0 additions & 16 deletions tests/resources/MockFeatureWrapper.ts

This file was deleted.

1 change: 0 additions & 1 deletion tests/resources/Types.ts

This file was deleted.

139 changes: 100 additions & 39 deletions tests/unit/FeatureWrapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,129 @@ import { mount } from "@vue/test-utils";
import { test, expect } from "vitest";

// Mock Component
import { MockFeatureWrapper } from "../resources/MockFeatureWrapper";
import { ClientCacheState, FeatureWrapper, HookEvents, OverrideBehaviour, SettingTypeOf, SettingValue, User, createFlagOverridesFromMap } from "../../src";
import ConfigCatPlugin, { type PluginOptions } from "../../src/plugins/ConfigCatPlugin";
import { ConfigCatClientMockBase, ConfigCatClientSnapshotMockBase } from "../mocks/ConfigCatClientMocks";

test("The default slot should render when the isFeatureFlagEnabled value is true", () => {
const wrapper = mount(MockFeatureWrapper, {
data() {
return {
isFeatureFlagEnabled: true,
};
},
slots: {
default: "<div>the new feature</div>",
},
});
const featureFlagKey = "isFeatureFlagEnabled";

// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>the new feature</div>");
});
const pluginOptions: PluginOptions = {
sdkKey: "local-only",
clientOptions: {
flagOverrides: createFlagOverridesFromMap({ [featureFlagKey]: true }, OverrideBehaviour.LocalOnly)
}
};

test("The default slot should not render when the isFeatureFlagEnabled value is false", () => {
const wrapper = mount(MockFeatureWrapper, {
data() {
return {
isFeatureFlagEnabled: false,
};
const wrapper = mount(FeatureWrapper, {
global: {
plugins: [[ConfigCatPlugin, pluginOptions]]
},
props: {
featureKey: featureFlagKey
},
slots: {
default: "<div>the new feature</div>",
else: "<div>feature is not enabled</div>",
loading: "<div>component is loading</div>",
},
});

// Assert the rendered text of the component
expect(wrapper.html()).not.toContain("<div>the new feature</div>");
try {
// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>the new feature</div>");
}
finally {
wrapper.unmount();
}
});

test("The else slot should render when the isFeatureFlagEnabled value is false", () => {
const wrapper = mount(MockFeatureWrapper, {
data() {
return {
isFeatureFlagEnabled: false,
};
const featureFlagKey = "isFeatureFlagEnabled";

const pluginOptions: PluginOptions = {
sdkKey: "local-only",
clientOptions: {
flagOverrides: createFlagOverridesFromMap({ [featureFlagKey]: false }, OverrideBehaviour.LocalOnly)
}
};

const wrapper = mount(FeatureWrapper, {
global: {
plugins: [[ConfigCatPlugin, pluginOptions]]
},
props: {
featureKey: featureFlagKey
},
slots: {
else: "<div>feature not enabled</div>",
default: "<div>the new feature</div>",
else: "<div>feature is not enabled</div>",
loading: "<div>component is loading</div>",
},
});

// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>feature not enabled</div>");
try {
// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>feature is not enabled</div>");
}
finally {
wrapper.unmount();
}
});

test("The loading slot should render when the isFeatureFlagEnabled value is neither true nor false", () => {
const wrapper = mount(MockFeatureWrapper, {
data() {
return {
isFeatureFlagEnabled: null,
};
test("The loading slot should render when the client is still initializing", () => {
const featureFlagKey = "isFeatureFlagEnabled";

// To test this scenario, we can't use flag overrides because there's no initialization period in that case,
// feature flag values provided by flag overrides are available immediately.
// So, we have to take the hard way: we need to provide a mock implementation of `IConfigCatClient` to the component.

const clientMock = new class extends ConfigCatClientMockBase {
snapshot() {
// Returning a snapshot with NoFlagData will cause the component to take the "async" way on component initialization (see onBeforeMount).
return new ConfigCatClientSnapshotMockBase(ClientCacheState.NoFlagData);
}

getValueAsync<T extends SettingValue>(key: string, defaultValue: T, user?: User | undefined): Promise<SettingTypeOf<T>> {
if (key === featureFlagKey) {
return new Promise(_ => {}); // by returning a Promise which never fullfils, we can make the initialization period infinite
}

return super.getValueAsync(key, defaultValue, user);
}

off<TEventName extends keyof HookEvents>(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this {
if (eventName == "configChanged") {
// The component unsubscribes from this event on component teardown (see onUnmounted).
// This is irrelevant in the case of this test, so a no-op will be fine.
return this;
}

return super.off(eventName, listener);
}
}();

const wrapper = mount(FeatureWrapper, {
global: {
provide: {
configCatClient: clientMock
}
},
props: {
featureKey: featureFlagKey
},
slots: {
loading: "<div>loading...</div>",
default: "<div>the new feature</div>",
else: "<div>feature is not enabled</div>",
loading: "<div>component is loading</div>",
},
});

// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>loading...</div>");
try {
// Assert the rendered text of the component
expect(wrapper.html()).toContain("<div>component is loading</div>");
}
finally {
wrapper.unmount();
}
});