UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

222 lines (212 loc) 6.72 kB
import { describe, test, expect } from "vitest"; import { InitData, InitDataProvider, InitQuery } from "../shared"; import Context from "./Context"; import splits from "../../test/splits"; import commitConfig from "../../test/commitConfig"; import reducedExpression from "../../test/expression"; import Logger from "./Logger"; const traceId = "test-trace"; const token = "test-token"; const initDataRefreshIntervalMs = 10_000; const initQuery: InitQuery = { type: "StoredQuery", id: "test-query" }; const variableValues = {}; const logger = new Logger({ id: "test-logger", traceId, token, remoteLoggingMode: "off", remoteFlushIntervalMs: null, remoteLoggingEndpointUrl: "", localLogger: () => { // Noop }, logsHandler: () => { // Noop }, }); const initData: InitData = { commitId: 1, hash: "1", splits, commitConfig, reducedExpression, }; function newContext( options: Partial<{ initData: InitData | null; initDataProvider: InitDataProvider | null; shouldRefreshInitDataOnCreate: boolean; shouldSkipInitDataUpdateOnRefresh: boolean; }> ): Context { return new Context({ traceId, initData: null, lastInitDataRefreshTime: null, initDataProvider: null, initDataRefreshIntervalMs, shouldRefreshInitData: false, shouldRefreshInitDataOnCreate: false, shouldSkipInitDataUpdateOnRefresh: false, query: null, initQuery, variableValues, logger, cacheSize: 100, override: null, ...options, }); } function newInitDataProvider( initialCommitId: number ): InitDataProvider & { getCallCount: () => number } { let commitId = initialCommitId; return { getName: () => "testProvider", getCallCount: () => commitId - initialCommitId, getInitData: () => { const data = { ...initData, commitId, hash: commitId.toString() }; commitId += 1; return Promise.resolve(data); }, getHashData: () => { const data = { commitId, hash: commitId.toString() }; commitId += 1; return Promise.resolve(data); }, }; } describe("Context", () => { describe("readiness", () => { describe("no init data provider", () => { test("no initial data", () => { const context = newContext({}); let listenerCallCount = 0; context.addUpdateListener((hash, meta) => { listenerCallCount += 1; expect(meta).toEqual({ becameReady: listenerCallCount === 1, updateTrigger: "hydration", hasUpdated: true, }); }); expect(context.isReady()).toBe(false); context.hydrate(traceId, { initData, lastInitDataRefreshTime: null, override: null, variableValues: {}, }); expect(context.isReady()).toBe(true); context.hydrate(traceId, { initData, lastInitDataRefreshTime: null, override: null, variableValues: {}, }); expect(listenerCallCount).toBe(1); }); test("with initial data", () => { const context = newContext({ initData }); expect(context.isReady()).toBe(true); }); }); describe("with init data provider", () => { test("no initial data and hydration", () => { const initDataProvider = newInitDataProvider(initData.commitId); const context = newContext({ initDataProvider }); expect(context.isReady()).toBe(false); context.hydrate(traceId, { initData, lastInitDataRefreshTime: null, override: null, variableValues: {}, }); expect(context.isReady()).toBe(false); context.hydrate(traceId, { initData, lastInitDataRefreshTime: Date.now(), override: null, variableValues: {}, }); expect(context.isReady()).toBe(true); expect(initDataProvider.getCallCount()).toBe(0); }); test("with initial data and initialization", async () => { const initDataProvider = newInitDataProvider(initData.commitId); const context = newContext({ initData, initDataProvider }); let listenerCallCount = 0; context.addUpdateListener((hash, meta) => { listenerCallCount += 1; expect(meta).toEqual({ becameReady: listenerCallCount === 1, updateTrigger: "initDataProvider", hasUpdated: false, }); }); expect(context.isReady()).toBe(false); await context.initIfNeeded(traceId, 1); expect(context.isReady()).toBe(true); context.hydrate(traceId, { initData, lastInitDataRefreshTime: Date.now(), override: null, variableValues: {}, }); expect(listenerCallCount).toBe(1); expect(initDataProvider.getCallCount()).toBe(1); }); test("with initial data and outdated provider", async () => { const initDataProvider = newInitDataProvider(-10); const context = newContext({ initData, initDataProvider }); let listenerCalled = false; context.addUpdateListener(() => { listenerCalled = true; }); expect(context.isReady()).toBe(false); await context.initIfNeeded(traceId, 1); expect(context.isReady()).toBe(false); expect(listenerCalled).toBe(false); expect(initDataProvider.getCallCount()).toBe(1); }); }); }); describe("override", () => { test("update triggers notification", () => { const context = newContext({}); let listenerCallCount = 0; context.addUpdateListener((hash, meta) => { listenerCallCount += 1; expect(meta).toEqual({ becameReady: false, updateTrigger: "override", hasUpdated: true, }); }); context.setOverride(traceId, { root: {} }); context.setOverride(traceId, { root: {} }); expect(listenerCallCount).toBe(1); }); }); test("shouldSkipInitDataUpdateOnRefresh", async () => { const initDataProvider = newInitDataProvider(initData.commitId + 1); const context = newContext({ initData, initDataProvider, shouldRefreshInitDataOnCreate: true, shouldSkipInitDataUpdateOnRefresh: true, }); let listenerCallCount = 0; context.addUpdateListener((hash, meta) => { expect(meta).toEqual({ becameReady: false, updateTrigger: "initDataProvider", hasUpdated: false, }); listenerCallCount += 1; }); await context.initIfNeeded(traceId, 1); expect(listenerCallCount).toBe(1); expect(initDataProvider.getCallCount()).toBe(1); }); });