Skip to main content
Glama

Bucket Feature Flags MCP Server

Official
by reflagcom
usage.test.tsx30.7 kB
import React from "react"; import { render, renderHook, waitFor } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi, } from "vitest"; import { ReflagClient } from "@reflag/browser-sdk"; import { BootstrappedFlags, ReflagBootstrappedProps, ReflagBootstrappedProvider, ReflagClientProvider, ReflagProps, ReflagProvider, useClient, useFlag, useIsLoading, useOnEvent, useRequestFeedback, useSendFeedback, useTrack, useUpdateCompany, useUpdateOtherContext, useUpdateUser, } from "../src"; const events: string[] = []; const originalConsoleError = console.error.bind(console); afterEach(() => { events.length = 0; console.error = originalConsoleError; }); const company = { id: "123", name: "test" }; const user = { id: "456", name: "test" }; const other = { test: "test" }; let keyIndex = 0; function getProvider(props: Omit<Partial<ReflagProps>, "publishableKey"> = {}) { const publishableKey = `KEY-${keyIndex++}`; return ( <ReflagProvider context={{ user, company, other }} publishableKey={publishableKey} {...props} /> ); } function getBootstrapProvider( bootstrapFlags: BootstrappedFlags, props: Omit<Partial<ReflagBootstrappedProps>, "publishableKey"> = {}, ) { const publishableKey = `KEY-${keyIndex++}`; return ( <ReflagBootstrappedProvider flags={bootstrapFlags} publishableKey={publishableKey} {...props} /> ); } const server = setupServer( http.post(/\/event$/, () => { events.push("EVENT"); return new HttpResponse( JSON.stringify({ success: true, }), { status: 200 }, ); }), http.post(/\/feedback$/, () => { events.push("FEEDBACK"); return new HttpResponse( JSON.stringify({ success: true, }), { status: 200 }, ); }), http.get(/\/features\/evaluated$/, () => { return new HttpResponse( JSON.stringify({ success: true, features: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, version: 2, }, }, def: { key: "def", isEnabled: true, targetingVersion: 2, }, }, }), { status: 200 }, ); }), http.post(/\/user$/, () => { return new HttpResponse( JSON.stringify({ success: true, }), { status: 200 }, ); }), http.post(/\/company$/, () => { return new HttpResponse( JSON.stringify({ success: true, }), { status: 200 }, ); }), http.post(/feedback\/prompting-init$/, () => { return new HttpResponse( JSON.stringify({ success: false, }), { status: 200 }, ); }), http.post(/\/features\/events$/, () => { return new HttpResponse( JSON.stringify({ success: false, }), { status: 200 }, ); }), ); beforeAll(() => server.listen({ onUnhandledRequest(request) { console.error("Unhandled %s %s", request.method, request.url); }, }), ); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeAll(() => { vi.spyOn(ReflagClient.prototype, "initialize"); vi.spyOn(ReflagClient.prototype, "stop"); }); beforeEach(() => { vi.clearAllMocks(); }); describe("<ReflagProvider />", () => { test("calls initialize", () => { const initialize = vi.spyOn(ReflagClient.prototype, "initialize"); const provider = getProvider({ apiBaseUrl: "https://apibaseurl.com", sseBaseUrl: "https://ssebaseurl.com", context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, enableTracking: false, appBaseUrl: "https://appbaseurl.com", staleTimeMs: 1001, timeoutMs: 1002, expireTimeMs: 1003, staleWhileRevalidate: true, fallbackFlags: ["flag2"], feedback: { enableAutoFeedback: true }, toolbar: { show: true }, }); render(provider); expect(initialize).toHaveBeenCalled(); }); test("only calls init once with the same args", () => { const node = getProvider(); const initialize = vi.spyOn(ReflagClient.prototype, "initialize"); const x = render(node); x.rerender(node); x.rerender(node); x.rerender(node); expect(initialize).toHaveBeenCalledOnce(); expect(ReflagClient.prototype.stop).not.toHaveBeenCalledOnce(); }); test("handles context changes", async () => { const { queryByTestId, rerender } = render( getProvider({ loadingComponent: <span data-testid="loading">Loading...</span>, children: <span data-testid="content">Content</span>, }), ); // Wait for content to be visible await waitFor(() => { expect(queryByTestId("content")).not.toBeNull(); }); // Change user context rerender( getProvider({ loadingComponent: <span data-testid="loading">Loading...</span>, user: { ...user, id: "new-user-id" }, children: <span data-testid="content">Content</span>, }), ); // Content should still be visible await waitFor(() => { expect(queryByTestId("content")).not.toBeNull(); }); // Change company context rerender( getProvider({ loadingComponent: <span data-testid="loading">Loading...</span>, company: { ...company, id: "new-company-id" }, children: <span data-testid="content">Content</span>, }), ); // Content should still be visible await waitFor(() => { expect(queryByTestId("content")).not.toBeNull(); }); }); }); describe("useFlag", () => { test("returns a loading state initially", async () => { const { result, unmount } = renderHook(() => useFlag("huddle"), { wrapper: ({ children }) => getProvider({ children }), }); // The flag should exist but may be loading or not depending on implementation expect(result.current.key).toBe("huddle"); expect(result.current.isEnabled).toBe(false); expect(result.current.config).toEqual({ key: undefined, payload: undefined, }); expect(typeof result.current.track).toBe("function"); expect(typeof result.current.requestFeedback).toBe("function"); unmount(); }); test("finishes loading", async () => { const { result, unmount } = renderHook(() => useFlag("huddle"), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(() => { expect(result.current).toStrictEqual({ key: "huddle", config: { key: undefined, payload: undefined }, isEnabled: false, isLoading: false, track: expect.any(Function), requestFeedback: expect.any(Function), }); }); unmount(); }); test("provides the expected values if flag is enabled", async () => { const { result, unmount } = renderHook(() => useFlag("abc"), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(() => { expect(result.current).toStrictEqual({ key: "abc", isEnabled: true, isLoading: false, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, }, track: expect.any(Function), requestFeedback: expect.any(Function), }); }); unmount(); }); }); describe("useTrack", () => { test("sends track request", async () => { const { result, unmount } = renderHook(() => useTrack(), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(async () => { await result.current("event", { test: "test" }); expect(events).toStrictEqual(["EVENT"]); }); unmount(); }); }); describe("useSendFeedback", () => { test("sends feedback", async () => { const { result, unmount } = renderHook(() => useSendFeedback(), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(async () => { await result.current({ flagKey: "huddles", score: 5, }); expect(events).toStrictEqual(["FEEDBACK"]); }); unmount(); }); }); describe("useRequestFeedback", () => { test("sends feedback", async () => { const requestFeedback = vi .spyOn(ReflagClient.prototype, "requestFeedback") .mockReturnValue(undefined); const { result, unmount } = renderHook(() => useRequestFeedback(), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(async () => { result.current({ flagKey: "huddles", title: "Test question", companyId: "456", }); expect(requestFeedback).toHaveBeenCalledOnce(); expect(requestFeedback).toHaveBeenCalledWith({ flagKey: "huddles", companyId: "456", title: "Test question", }); }); unmount(); }); }); describe("useUpdateUser", () => { test("updates user", async () => { const updateUser = vi .spyOn(ReflagClient.prototype, "updateUser") .mockResolvedValue(undefined); const { result: updateUserFn, unmount } = renderHook( () => useUpdateUser(), { wrapper: ({ children }) => getProvider({ children }), }, ); // todo: need this `waitFor` because useUpdateOtherContext // runs before `client` is initialized and then the call gets // lost. await waitFor(async () => { await updateUserFn.current({ optInHuddles: "true" }); expect(updateUser).toHaveBeenCalledWith({ optInHuddles: "true", }); }); unmount(); }); }); describe("useUpdateCompany", () => { test("updates company", async () => { const updateCompany = vi .spyOn(ReflagClient.prototype, "updateCompany") .mockResolvedValue(undefined); const { result: updateCompanyFn, unmount } = renderHook( () => useUpdateCompany(), { wrapper: ({ children }) => getProvider({ children }), }, ); // todo: need this `waitFor` because useUpdateOtherContext // runs before `client` is initialized and then the call gets // lost. await waitFor(async () => { await updateCompanyFn.current({ optInHuddles: "true" }); expect(updateCompany).toHaveBeenCalledWith({ optInHuddles: "true", }); }); unmount(); }); }); describe("useUpdateOtherContext", () => { test("updates other context", async () => { const updateOtherContext = vi .spyOn(ReflagClient.prototype, "updateOtherContext") .mockResolvedValue(undefined); const { result: updateOtherContextFn, unmount } = renderHook( () => useUpdateOtherContext(), { wrapper: ({ children }) => getProvider({ children }), }, ); // todo: need this `waitFor` because useUpdateOtherContext // runs before `client` is initialized and then the call gets // lost. await waitFor(async () => { await updateOtherContextFn.current({ optInHuddles: "true" }); expect(updateOtherContext).toHaveBeenCalledWith({ optInHuddles: "true", }); }); unmount(); }); }); describe("useClient", () => { test("gets the client", async () => { const { result: clientFn, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => getProvider({ children }), }); await waitFor(async () => { expect(clientFn.current).toBeDefined(); }); unmount(); }); }); describe("<ReflagBootstrappedProvider />", () => { test("renders with pre-fetched flags", () => { const bootstrapFlags: BootstrappedFlags = { context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, flags: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, version: 2, }, }, def: { key: "def", isEnabled: true, targetingVersion: 2, }, }, }; const { container } = render( getBootstrapProvider(bootstrapFlags, { apiBaseUrl: "https://apibaseurl.com", sseBaseUrl: "https://ssebaseurl.com", enableTracking: false, appBaseUrl: "https://appbaseurl.com", staleTimeMs: 1001, timeoutMs: 1002, expireTimeMs: 1003, staleWhileRevalidate: true, fallbackFlags: ["flag2"], feedback: { enableAutoFeedback: true }, toolbar: { show: true }, children: <span>Test Content</span>, }), ); expect(container).toBeDefined(); }); test("renders in bootstrap mode", () => { const bootstrapFlags: BootstrappedFlags = { context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, flags: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, }, }, }; const { container } = render( getBootstrapProvider(bootstrapFlags, { children: <span>Bootstrap Content</span>, }), ); expect(container).toBeDefined(); }); // Removed test "does not initialize when no flags are provided" // because ReflagBootstrappedProvider requires flags to be provided test("shows content after initialization", async () => { const bootstrapFlags: BootstrappedFlags = { context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, flags: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, }, }, }; const { container } = render( getBootstrapProvider(bootstrapFlags, { loadingComponent: <span data-testid="loading">Loading...</span>, children: <span data-testid="bootstrap-content">Content</span>, }), ); // Content should eventually be visible await waitFor(() => { expect( container.querySelector('[data-testid="bootstrap-content"]'), ).not.toBeNull(); }); }); // Removed test "shows loading component when no flags are provided" // because ReflagBootstrappedProvider requires flags to be provided }); describe("useFlag with ReflagBootstrappedProvider", () => { test("returns bootstrapped flag values", async () => { const bootstrapFlags: BootstrappedFlags = { context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, flags: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, version: 2, }, }, def: { key: "def", isEnabled: true, targetingVersion: 2, }, }, }; const { result, unmount } = renderHook(() => useFlag("abc"), { wrapper: ({ children }) => getBootstrapProvider(bootstrapFlags, { children }), }); await waitFor(() => { expect(result.current).toStrictEqual({ key: "abc", isEnabled: true, isLoading: false, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, }, track: expect.any(Function), requestFeedback: expect.any(Function), }); }); unmount(); }); test("returns disabled flag for non-existent flags", async () => { const bootstrapFlags: BootstrappedFlags = { context: { user: { id: "456", name: "test" }, company: { id: "123", name: "test" }, other: { test: "test" }, }, flags: { abc: { key: "abc", isEnabled: true, targetingVersion: 1, }, }, }; const { result, unmount } = renderHook(() => useFlag("nonexistent"), { wrapper: ({ children }) => getBootstrapProvider(bootstrapFlags, { children }), }); await waitFor(() => { expect(result.current).toStrictEqual({ key: "nonexistent", isEnabled: false, isLoading: false, config: { key: undefined, payload: undefined, }, track: expect.any(Function), requestFeedback: expect.any(Function), }); }); unmount(); }); // Removed test "returns loading state when no flags are bootstrapped" // because ReflagBootstrappedProvider requires flags to be provided }); describe("<ReflagClientProvider />", () => { test("renders with external client and optional loadingComponent", async () => { const client = new ReflagClient({ publishableKey: "test-key", user, company, other, }); const { container } = render( <ReflagClientProvider client={client}> <span data-testid="content">Test Content</span> </ReflagClientProvider>, ); expect(container.querySelector('[data-testid="content"]')).not.toBeNull(); }); test("renders with external client and loadingComponent", async () => { const client = new ReflagClient({ publishableKey: "test-key", user, company, other, }); const { container } = render( <ReflagClientProvider client={client} loadingComponent={<span data-testid="loading">Loading...</span>} > <span data-testid="content">Test Content</span> </ReflagClientProvider>, ); // Initially may show loading or content depending on client state expect(container).toBeDefined(); }); test("provides client to child components", async () => { const client = new ReflagClient({ publishableKey: "test-key", user, company, other, }); const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagClientProvider client={client}>{children}</ReflagClientProvider> ), }); expect(result.current).toBe(client); // Verify that the external client maintains its context const context = result.current.getContext(); expect(context.user).toEqual(user); expect(context.company).toEqual(company); expect(context.other).toEqual(other); unmount(); }); test("handles client state changes", async () => { const client = new ReflagClient({ publishableKey: "test-key-state-changes", user, company, other, }); const { container } = render( <ReflagClientProvider client={client} loadingComponent={<span data-testid="client-loading">Loading...</span>} > <span data-testid="client-content">Content</span> </ReflagClientProvider>, ); // The component should handle state changes properly expect( container.querySelector('[data-testid="client-content"]') || container.querySelector('[data-testid="client-loading"]'), ).not.toBeNull(); }); test("works with useFlag hook", async () => { const client = new ReflagClient({ publishableKey: "test-key", user, company, other, }); const { result, unmount } = renderHook(() => useFlag("test-flag"), { wrapper: ({ children }) => ( <ReflagClientProvider client={client}>{children}</ReflagClientProvider> ), }); expect(result.current.key).toBe("test-flag"); expect(typeof result.current.track).toBe("function"); expect(typeof result.current.requestFeedback).toBe("function"); unmount(); }); }); describe("ReflagProvider with deprecated properties", () => { test("works with deprecated user property", async () => { const deprecatedUser = { id: "deprecated-user", name: "Deprecated User" }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider context={{}} publishableKey="test-key-1" user={deprecatedUser} > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); expect(context.user).toEqual(deprecatedUser); expect(context.company).toBeUndefined(); expect(context.other).toEqual({}); }); unmount(); }); test("works with deprecated company property", async () => { const deprecatedCompany = { id: "deprecated-company", name: "Deprecated Company", }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider company={deprecatedCompany} context={{}} publishableKey="test-key-2" > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); expect(context.company).toEqual(deprecatedCompany); expect(context.user).toBeUndefined(); expect(context.other).toEqual({}); }); unmount(); }); test("works with deprecated otherContext property", async () => { const deprecatedOtherContext = { workspace: "deprecated-workspace" }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider context={{}} otherContext={deprecatedOtherContext} publishableKey="test-key-3" > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); expect(context.other).toEqual(deprecatedOtherContext); expect(context.user).toBeUndefined(); expect(context.company).toBeUndefined(); }); unmount(); }); test("context property overrides deprecated properties", async () => { const contextUser = { id: "context-user", name: "Context User" }; const contextCompany = { id: "context-company", name: "Context Company" }; const contextOther = { workspace: "context-workspace" }; const deprecatedUser = { id: "deprecated-user", name: "Deprecated User" }; const deprecatedCompany = { id: "deprecated-company", name: "Deprecated Company", }; const deprecatedOtherContext = { workspace: "deprecated-workspace" }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider company={deprecatedCompany} context={{ user: contextUser, company: contextCompany, other: contextOther, }} otherContext={deprecatedOtherContext} publishableKey="test-key-4" user={deprecatedUser} > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); // The context property should override deprecated properties expect(context.user).toEqual(contextUser); expect(context.company).toEqual(contextCompany); expect(context.other).toEqual(contextOther); }); unmount(); }); test("merges deprecated properties with context", async () => { const contextUser = { id: "context-user", email: "context@example.com" }; const deprecatedUser = { id: "deprecated-user", name: "Deprecated User" }; const deprecatedCompany = { id: "deprecated-company", name: "Deprecated Company", }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider company={deprecatedCompany} context={{ user: contextUser, }} publishableKey="test-key-5" user={deprecatedUser} > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); // The context user should override the deprecated user, // but deprecated company should still be present expect(context.user).toEqual(contextUser); expect(context.company).toEqual(deprecatedCompany); expect(context.other).toEqual({}); }); unmount(); }); test("handles all deprecated properties together", async () => { const deprecatedUser = { id: "deprecated-user", name: "Deprecated User" }; const deprecatedCompany = { id: "deprecated-company", name: "Deprecated Company", }; const deprecatedOtherContext = { workspace: "deprecated-workspace", feature: "test", }; const { result, unmount } = renderHook(() => useClient(), { wrapper: ({ children }) => ( <ReflagProvider company={deprecatedCompany} context={{}} otherContext={deprecatedOtherContext} publishableKey="test-key-6" user={deprecatedUser} > {children} </ReflagProvider> ), }); await waitFor(() => { expect(result.current).toBeDefined(); const context = result.current.getContext(); // All deprecated properties should be properly set expect(context.user).toEqual(deprecatedUser); expect(context.company).toEqual(deprecatedCompany); expect(context.other).toEqual(deprecatedOtherContext); }); unmount(); }); }); describe("useIsLoading", () => { test("returns loading state during initialization", async () => { const { result, unmount } = renderHook(() => useIsLoading(), { wrapper: ({ children }) => getProvider({ children }), }); // Should be loading initially expect(result.current).toBe(true); // Wait for initialization to complete await waitFor(() => { expect(result.current).toBe(false); }); unmount(); }); test("throws error when used outside provider", () => { const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => { // Silence console.error during test }); expect(() => { renderHook(() => useIsLoading()); }).toThrow( "ReflagProvider is missing. Please ensure your component is wrapped with a ReflagProvider.", ); consoleErrorSpy.mockRestore(); }); }); describe("useOnEvent", () => { test("subscribes to flagsUpdated event", async () => { const eventHandler = vi.fn(); const client = new ReflagClient({ publishableKey: "test-key-events", user, company, other, }); const { unmount } = renderHook( () => useOnEvent("flagsUpdated", eventHandler), { wrapper: ({ children }) => ( <ReflagClientProvider client={client}> {children} </ReflagClientProvider> ), }, ); // Initialize the client to trigger events await client.initialize(); // Wait for the event to be triggered await waitFor(() => { expect(eventHandler).toHaveBeenCalled(); }); unmount(); }); test("works with external client parameter", async () => { const eventHandler = vi.fn(); const client = new ReflagClient({ publishableKey: "test-key-external", user, company, other, }); const { unmount } = renderHook(() => useOnEvent("flagsUpdated", eventHandler, client), ); // Initialize the client to trigger events await client.initialize(); // Wait for the event to be triggered await waitFor(() => { expect(eventHandler).toHaveBeenCalled(); }); unmount(); }); test("cleans up event listeners on unmount", async () => { const eventHandler = vi.fn(); const client = new ReflagClient({ publishableKey: "test-key-cleanup", user, company, other, }); // Mock the addHook method to return a cleanup function that we can spy on const cleanupSpy = vi.fn(); const addHookSpy = vi .spyOn(client["hooks"], "addHook") .mockReturnValue(cleanupSpy); const { unmount } = renderHook( () => useOnEvent("flagsUpdated", eventHandler), { wrapper: ({ children }) => ( <ReflagClientProvider client={client}> {children} </ReflagClientProvider> ), }, ); // Verify that addHook was called with the correct parameters expect(addHookSpy).toHaveBeenCalledWith("flagsUpdated", eventHandler); unmount(); // Verify that the cleanup function was called expect(cleanupSpy).toHaveBeenCalled(); addHookSpy.mockRestore(); }); test("throws error when used outside provider without client parameter", () => { const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => { // Silence console.error during test }); const eventHandler = vi.fn(); expect(() => { renderHook(() => useOnEvent("flagsUpdated", eventHandler)); }).toThrow( "ReflagProvider is missing and no client was provided. Please ensure your component is wrapped with a ReflagProvider.", ); consoleErrorSpy.mockRestore(); }); test("handles multiple event subscriptions", async () => { const flagsHandler = vi.fn(); const stateHandler = vi.fn(); const client = new ReflagClient({ publishableKey: "test-key-multiple", user, company, other, }); const { unmount } = renderHook( () => { useOnEvent("flagsUpdated", flagsHandler); useOnEvent("stateUpdated", stateHandler); }, { wrapper: ({ children }) => ( <ReflagClientProvider client={client}> {children} </ReflagClientProvider> ), }, ); // Initialize the client to trigger events await client.initialize(); // Wait for both events to be triggered await waitFor(() => { expect(flagsHandler).toHaveBeenCalled(); expect(stateHandler).toHaveBeenCalled(); }); unmount(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reflagcom/bucket-javascript-sdk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server