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
text/typescript
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);
});
});