{"_id":"@avsbhq/test","_rev":"3-9fb3d3d0890389e51061f1bde67963ae","name":"@avsbhq/test","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@avsbhq/test","version":"1.0.0","_id":"@avsbhq/test@1.0.0","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-test#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"5914320ea2a61c8a37dd9da29a55164ea84fb517","tarball":"https://registry.npmjs.org/@avsbhq/test/-/test-1.0.0.tgz","fileCount":16,"integrity":"sha512-nCFWCQzw2mJwPebEbM6qbapE/cO3wrfCyahRA5MbzOx/dljPdNyJNxMqs8+2WUZWWi3jmZxMScaccYKJCNS3qQ==","signatures":[{"sig":"MEYCIQCykJk4N+PeY06eo7Uoxj6N3GDDRKFLVfm1Fk6PqSvAjAIhAO4/7BthhThvAe46qijUl3fiVP/qp4ZxmR9tIirH8k+b","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":73685},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./jest":{"types":"./dist/jest.d.ts","import":"./dist/jest.js","require":"./dist/jest.cjs"},"./vitest":{"types":"./dist/vitest.d.ts","import":"./dist/vitest.js","require":"./dist/vitest.cjs"}},"gitHead":"8cd6226e120fe67c198b2f1db0087cd38b4a4060","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-test"},"_npmVersion":"10.9.2","description":"Test utilities for the [A vs B](https://app.avsb.cloud) SDK.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"@avsbhq/core":"1.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","jsdom":"^25.0.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.5.0","@types/react":"^19.0.0","@testing-library/react":"^16.0.0"},"peerDependencies":{"react":">=18.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/test_1.0.0_1780034360113_0.88542682596392","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@avsbhq/test","version":"1.0.1","_id":"@avsbhq/test@1.0.1","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"homepage":"https://github.com/main1479/a-vs-b/tree/main/packages/avsb-test#readme","bugs":{"url":"https://github.com/main1479/a-vs-b/issues"},"dist":{"shasum":"bac18fd74ff57d3adb4852e6cb40f66d16bf04a7","tarball":"https://registry.npmjs.org/@avsbhq/test/-/test-1.0.1.tgz","fileCount":16,"integrity":"sha512-KVd7sgDcgroKFd8k3X5SoE9ciLhWrx9FdzyMaH5GQ1V7PkgEZGc5X6uG6LXVPG89q1dWziUpPuWXBqrhpYWwMA==","signatures":[{"sig":"MEUCIQCgMKGPvaVgVrvpGVH9EW4zUW/6Q0Vd80WmZX/UFSCaiwIgHgIzr+yeYO+LaXJl+qeCWK/DQfh24oIQIy7rXDbcDBc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":73673},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./jest":{"types":"./dist/jest.d.ts","import":"./dist/jest.js","require":"./dist/jest.cjs"},"./vitest":{"types":"./dist/vitest.d.ts","import":"./dist/vitest.js","require":"./dist/vitest.cjs"}},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"repository":{"url":"git+https://github.com/main1479/a-vs-b.git","type":"git","directory":"packages/avsb-test"},"_npmVersion":"10.9.8","description":"Test utilities for the [A vs B](https://app.avsb.cloud) SDK.","directories":{},"_nodeVersion":"22.22.3","dependencies":{"@avsbhq/core":"1.4.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","jsdom":"^25.0.0","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.5.0","@types/react":"^19.0.0","@testing-library/react":"^16.0.0"},"peerDependencies":{"react":">=18.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/test_1.0.1_1785346450635_0.7540584501432168","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@avsbhq/test","version":"1.1.0","description":"Test utilities for the A vs B SDKs: a browser-shaped mock, a server-shaped mock, real test clients, datafile fixtures, a fluent flag builder, and evaluation recorders.","keywords":["avsb","feature-flags","ab-testing","experiments","testing","mock","vitest","jest"],"license":"MIT","type":"module","sideEffects":false,"main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./fixtures":{"import":{"types":"./dist/fixtures.d.ts","default":"./dist/fixtures.js"},"require":{"types":"./dist/fixtures.d.cts","default":"./dist/fixtures.cjs"}},"./react":{"import":{"types":"./dist/react.d.ts","default":"./dist/react.js"},"require":{"types":"./dist/react.d.cts","default":"./dist/react.cjs"}},"./vitest":{"import":{"types":"./dist/vitest.d.ts","default":"./dist/vitest.js"},"require":{"types":"./dist/vitest.d.cts","default":"./dist/vitest.cjs"}},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-test"},"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-test#readme","bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"react":">=18.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"dependencies":{"@avsbhq/browser":"^1.3.1","@avsbhq/core":"^1.4.0"},"devDependencies":{"@testing-library/react":"^16.0.0","@types/react":"^19.0.0","jsdom":"^25.0.0","react":"^19.0.0","react-dom":"^19.0.0","tsup":"^8.0.0","typescript":"^5.5.0","vitest":"^4.1.4"},"_id":"@avsbhq/test@1.1.0","gitHead":"5eed46d5944b522ba0aa34e82850c98bb91667fe","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-wQ1LPL54mwzJvbQOxgqxZir4G9r5jMn/NjPiX/tLRifTSggyb3GpjgASElC1uNgKplJxSPOfhPxhqmC2R/97BA==","shasum":"5af512a479660e7079264079bb5e49b388c4da80","tarball":"https://registry.npmjs.org/@avsbhq/test/-/test-1.1.0.tgz","fileCount":21,"unpackedSize":185811,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC7NJnJNE/+ycGGm8vKipx9ISTO+EQaBpfj3NJWJSfvKwIhAJhmK9Y1H4Qu3JRuLqFCCZq0D+INdCLaqX7MXzUfNYng"}]},"_npmUser":{"name":"main1479","email":"m.main2402@gmail.com"},"directories":{},"maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/test_1.1.0_1785971474414_0.2790090013027775"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-29T05:59:19.985Z","modified":"2026-08-05T23:11:14.728Z","1.0.0":"2026-05-29T05:59:20.238Z","1.0.1":"2026-07-29T17:34:10.802Z","1.1.0":"2026-08-05T23:11:14.560Z"},"bugs":{"url":"https://github.com/avsbhq/a-vs-b/issues"},"homepage":"https://github.com/avsbhq/a-vs-b/tree/main/packages/avsb-test#readme","repository":{"type":"git","url":"git+https://github.com/avsbhq/a-vs-b.git","directory":"packages/avsb-test"},"description":"Test utilities for the A vs B SDKs: a browser-shaped mock, a server-shaped mock, real test clients, datafile fixtures, a fluent flag builder, and evaluation recorders.","maintainers":[{"name":"main1479","email":"m.main2402@gmail.com"}],"readme":"# @avsbhq/test\n\nTest utilities for the [A vs B](https://app.avsb.cloud) SDKs.\n\nA browser-shaped mock client, a server-shaped mock with `forUser`, a real offline client, a React test provider, a fluent flag builder, evaluation recorders, and fixture factories, so you can write deterministic tests without a live SDK connection.\n\nTwo ways to test, and the choice matters:\n\n| Helper             | What it is                                   | Use it when                                                                                         |\n| ------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `createTestClient` | a REAL `AvsbClient` over a fixture datafile  | you want the real evaluator, the real type checks, real `source` values and real exposure behaviour |\n| `createMockClient` | a hand-driven stand-in with the same surface | you want to drive values directly, or assert on what your code asked for                            |\n\n---\n\n## 1. Install\n\n```bash\nnpm install --save-dev @avsbhq/test\n```\n\nWorks with vitest and jest. `@avsbhq/core` and `@avsbhq/browser` come with it (the browser client is what `createTestClient` returns). React is an optional peer, needed only for `@avsbhq/test/react`.\n\n### Entry points\n\n| Import from             | What it carries                                                                          | Needs React |\n| ----------------------- | ---------------------------------------------------------------------------------------- | ----------- |\n| `@avsbhq/test`          | the mocks, `TestData`, the recorders, `resolveTestClient`, and everything on `/fixtures` | no          |\n| `@avsbhq/test/fixtures` | `createTestDatafile`, `createTestContext`, `createMultiTestContext`, `createTestClient`  | no          |\n| `@avsbhq/test/react`    | `AvsbTestProvider`, `useAvsbTestClient`                                                  | yes         |\n| `@avsbhq/test/vitest`   | `setupAvsbTest`                                                                          | no          |\n\nThe split is not a nicety. A root barrel that imports React does not RESOLVE when React is absent, so while `AvsbTestProvider` sat on the root, a Vue, Svelte, or plain-Node project could not import `createMockClient` at all and \"React is an optional peer\" was false. The root is now React-free and only `/react` reaches for it.\n\n`/fixtures` exists for a different reason: `@avsbhq/vue/testing` and `@avsbhq/svelte/testing` re-export from it and bundle it at build time, so it has to stay free of the mocks as well as of React. Import from it directly when you want nothing else pulled in.\n\n### Vitest lifecycle helper\n\n```ts\nimport { setupAvsbTest } from '@avsbhq/test/vitest';\n\ndescribe('checkout', () => {\n  const { getClient } = setupAvsbTest({ flags: { 'checkout-v2': true } });\n\n  it('reads the flag', () => {\n    expect(getClient().getBoolFlag('checkout-v2', false).value).toBe(true);\n  });\n});\n```\n\nIt registers `beforeEach`/`afterEach` for you: a fresh mock per test, closed afterwards.\n\n### Jest\n\nThere is no jest preset. Earlier docs showed `{ preset: '@avsbhq/test/jest' }` and a vitest `plugins: [avsbPlugin()]` entry; neither existed, and the preset could not have worked (jest reads `module.exports`, and the preset named `ts-jest`, which this package does not depend on). The two real lines are:\n\n```js\nimport { createMockClient } from '@avsbhq/test';\n\nlet client;\nbeforeEach(() => {\n  client = createMockClient({ flags: { 'checkout-v2': true } });\n});\n```\n\n---\n\n## 2. Quickstart\n\n```ts\nimport { createMockClient, flagsFromTestData, TestData } from '@avsbhq/test';\n\n// 1. Describe the flag configuration.\nconst entry = TestData.flag('checkout-v2')\n  .booleanFlag()\n  .variationForUser('u_paying', true)\n  .fallthroughVariation(false)\n  .build();\n\n// 2. Build the mock from it.\nconst client = createMockClient(flagsFromTestData([entry]));\n\n// 3. Evaluate.\nexpect(client.getBoolFlag('checkout-v2', false).value).toBe(false);\n\nclient.identify({ kind: 'user', key: 'u_paying' });\nexpect(client.getBoolFlag('checkout-v2', false).value).toBe(true);\n```\n\n`flagsFromTestData(entries)` is the bridge. It returns exactly the options the mocks take:\n\nThe shapes quoted on this page name types from three places, all of them already on disk once this package is installed:\n\n```ts\nimport type { MockClient, MockClientOptions, TestDataEntry } from '@avsbhq/test';\nimport type { AvsbClient } from '@avsbhq/browser';\nimport type { EvalContext, FlagDatafile, FlagDatafileEntry } from '@avsbhq/core';\nimport type { ReactNode } from 'react';\n```\n\n```ts\ninterface MockFlagState {\n  /** flagKey to value: what every context gets unless overridden. */\n  flags: Record<string, unknown>;\n  /** `${flagKey}:${contextKey}` to value. Beats `flags`. */\n  flagsByUser: Record<string, unknown>;\n}\n\nfunction flagsFromTestData(entries: readonly TestDataEntry[]): MockFlagState;\n```\n\nEarlier docs taught `createMockClient({ flags: [td.build()] })`: an ARRAY where a map belongs, which did not typecheck and evaluated to defaults at runtime. That is what this function replaces.\n\n---\n\n## 3. `createMockClient` (browser-shaped)\n\nMirrors `AvsbClient` from `@avsbhq/browser`: one bound context, `identify`, `subscribe`, `getSnapshot`.\n\n```ts\nconst client = createMockClient({\n  flags: { 'dark-mode': true, theme: 'midnight', 'max-results': 50 },\n  flagsByUser: { 'checkout-v2:u_trial': false, 'checkout-v2:u_paid': true },\n  context: { kind: 'user', key: 'u_1', plan: 'pro' },\n  ready: true,\n});\n```\n\nIt implements the WHOLE client surface, not a convenient subset, because the framework adapters type their injection seams against the real client. `@avsbhq/angular` is the strictest: `AvsbService` calls `isReady()`, `getInitResult()` and `on('ready')` in its constructor, so a mock missing one of those is a TypeError on injection, not a weaker mock.\n\n| Group             | Methods                                                                                                                           |\n| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |\n| Lifecycle         | `onReady(options?)`, `isReady()`, `getInitResult()`, `refresh()`, `flush()`, `close()`                                            |\n| Identity          | `identify(ctx)`, `updateAttributes(partial, kind?)`, `alias(previous, next)`, `getContext()`, `reset()`                           |\n| Reading flags     | `getFlag`, `getBoolFlag`, `getStringFlag`, `getNumberFlag`, `getJsonFlag` (each taking `GetFlagOptions`), `getAllFlags(options?)` |\n| Store contract    | `subscribe(flagKey, listener)`, `getSnapshot(flagKey, default)`                                                                   |\n| Tracking          | `track(eventKey, payload?)`, `on(event, cb)`                                                                                      |\n| Runtime overrides | `setOverrideForUser`, `setGlobalOverride`, `clearOverrideForUser`, `clearGlobalOverride`, `clearAllOverrides`                     |\n| Bootstrap         | `applyDatafileBootstrap(datafile)`                                                                                                |\n\nWhat the mock deliberately does NOT do, so you are not surprised:\n\n- `fireExposure` and `decideOptions` on `GetFlagOptions` are accepted (the signature has to match) and are inert, because a mock has no exposure pipeline to suppress. Use `createTestClient` when exposure behaviour is what you are testing.\n- `flush()` and `close()` have no transport to drain.\n- `applyDatafileBootstrap` records the datafile and merges each flag's default variation value into the flags map. It does not evaluate rules; a real datafile with real rules belongs in `createTestClient`.\n- Before it is ready, reads return `source: 'not_ready'`, never `not_found`, and record nothing.\n\nInspection helpers:\n\n```ts\nclient._tracked(); // [{ eventKey, payload }]\nclient._evaluations(); // [{ flagKey, value, contextKey }]\nclient._setFlag('dark-mode', false); // notifies subscribers, emits flagChange\nclient._markReady({ success: true, source: 'network' });\nclient._datafile(); // what applyDatafileBootstrap was given, or null\nclient._refreshCount(); // how many times refresh() was called\n```\n\n### Injecting it into a framework\n\nAngular takes it directly, because `MockClient` satisfies `AngularAvsbClient`:\n\n```ts\nimport { TestBed } from '@angular/core/testing';\nimport { createMockClient } from '@avsbhq/test';\nimport { provideAvsb } from '@avsbhq/angular';\n\nTestBed.configureTestingModule({\n  providers: [provideAvsb({ client: createMockClient({ flags: { 'checkout-v2': true } }) })],\n});\n```\n\n---\n\n## 4. `createMockServer` (server-shaped)\n\nMirrors `AvsbServer` from `@avsbhq/node`: context passed per call or bound with `forUser`, `track` carrying its context, purchases.\n\n```ts\nimport { createMockServer, flagsFromTestData, TestData } from '@avsbhq/test';\n\nconst server = createMockServer(\n  flagsFromTestData([\n    TestData.flag('checkout-v2')\n      .booleanFlag()\n      .variationForUser('u_paying', true)\n      .fallthroughVariation(false)\n      .build(),\n  ]),\n);\n\n// Inject it wherever your code takes an AvsbServer.\nconst user = server.forUser({ kind: 'user', key: 'u_paying' });\nexpect(user.getBoolFlag('checkout-v2', false).value).toBe(true);\n\nuser.track('purchase', { revenue: 99 });\nawait user.trackPurchase({ orderId: 'o_1', total: 99 });\n\nexpect(server._tracked()).toEqual([\n  { eventKey: 'purchase', payload: { revenue: 99 }, contextKey: 'u_paying' },\n]);\nexpect(server._purchases()).toHaveLength(1);\nexpect(user.getDecisionLog().entries).toHaveLength(1);\n```\n\nIt answers the same fixture through the same resolver as `createMockClient`, so one fixture drives both. Reads without a context return `source: 'not_ready'`, exactly like the real server, and `createMockServer({ ready: false })` lets you test the pre-ready path.\n\nInspection helpers: `_tracked()`, `_purchases()`, `_evaluations()`, `_exposures()` (manual exposures flagged), `_setFlag()`, `_markReady()`.\n\n---\n\n## 5. `TestData`: fluent flag builder\n\n```ts\nconst boolFlag = TestData.flag('checkout-v2')\n  .booleanFlag()\n  .variationForUser('u_1', true) // per-user\n  .variationForContext('organization', 'org_1', true) // per-context-kind\n  .fallthroughVariation(false) // everyone else\n  .build();\n\nconst strFlag = TestData.flag('theme')\n  .stringFlag()\n  .variationForUser('u_dark', 'midnight')\n  .fallthroughVariation('light')\n  .build();\n\nconst jsonFlag = TestData.flag('api-config')\n  .jsonFlag()\n  .fallthroughVariation({ timeout: 5000, retries: 3 })\n  .build();\n\nconst client = createMockClient(flagsFromTestData([boolFlag, strFlag, jsonFlag]));\n```\n\nThe mocks match a per-context override on the context KEY and do not compare kinds, so `variationForContext('organization', 'org_1', ...)` applies whenever the bound context carries the key `org_1`.\n\n---\n\n## 6. `AvsbTestProvider`: React component tests\n\nIt ships on `@avsbhq/test/react`, not on the root, so that the root stays importable without React.\n\n```tsx\nimport { render, screen } from '@testing-library/react';\nimport { AvsbTestProvider } from '@avsbhq/test/react';\n\ntest('shows the new checkout when the flag is on', () => {\n  render(\n    <AvsbTestProvider flags={{ 'new-checkout-flow': true }}>\n      <CheckoutButton />\n    </AvsbTestProvider>,\n  );\n  expect(screen.getByRole('button')).toHaveTextContent('New checkout');\n});\n```\n\nProps (breaking change B26). `flags`, `context` and `client` are the same three names the reference wrappers in `@avsbhq/vue`, `@avsbhq/svelte` and `@avsbhq/solid` use, so the shape is learned once:\n\n```ts\ninterface AvsbTestProviderProps {\n  /** Shorthand: flagKey to value. */\n  flags?: Record<string, unknown>;\n  /** The bound context. Default `{ kind: 'user', key: 'test-user' }`. */\n  context?: EvalContext;\n  /** A pre-built mock client. Wins over everything. */\n  client?: MockClient;\n  /** Full createMockClient options. Wins over `flags` and `context`. */\n  options?: MockClientOptions;\n  children: ReactNode;\n}\n```\n\nPrecedence: `client`, then `options`, then `flags` and `context` together, then an empty mock. Earlier docs showed `mockFlags`, which never existed. `resolveTestClient(props)` applies the same precedence rule in a wrapper of your own, and it lives on the ROOT entry because it touches no React.\n\nThis provider supplies THIS package's context, read by `useAvsbTestClient()`. To drive the real `@avsbhq/react` hooks, use `AvsbTestProvider` from `@avsbhq/react/testing`: it renders the real provider around a real client. This package cannot do that itself without depending on `@avsbhq/react`, which would point the dependency the wrong way.\n\n---\n\n## 7. `recordEvaluations`: assertion helpers\n\nThe assertions live on the recorder itself, not on `expect`. Nothing is registered\nwith your test runner, so they read the same under vitest, jest, or node:test, and\neach one throws with a message naming what was actually recorded.\n\n```ts\nimport { createMockClient, recordEvaluations } from '@avsbhq/test';\n\nconst client = createMockClient({\n  flags: { 'checkout-v2': true },\n  context: { kind: 'user', key: 'u_1' },\n});\nconst recorder = recordEvaluations(client);\n\nclient.getBoolFlag('checkout-v2', false);\nclient.track('items_added', { value: 3 });\n\nrecorder.toHaveEvaluated('checkout-v2');\nrecorder.toHaveEvaluated('checkout-v2').withContext({ kind: 'user', key: 'u_1' }).withValue(true);\n\nrecorder.toHaveTracked('items_added').withValue(3);\n\n// The raw rows, for anything the two matchers do not cover, including\n// asserting that a flag was never read.\nrecorder.records; // [{ flagKey, value, contextKey }]\nexpect(recorder.records.some((r) => r.flagKey === 'unknown-flag')).toBe(false);\n\nrecorder.stop(); // unsubscribe and clear\n```\n\n---\n\n## 8. Fixture factories and the real test client\n\nThese four also ship on `@avsbhq/test/fixtures`, the entry point with no React and no mocks in it. `@avsbhq/vue/testing` and `@avsbhq/svelte/testing` re-export them from there, so `createTestDatafile` from either of those and from `@avsbhq/test` is the same function rather than lookalikes that disagree about what \"a flag that is on\" means.\n\n`@avsbhq/solid/testing` still carries its own copy, deliberately: its `./testing` subpath declares a `solid` export condition pointing at raw source, and raw source shipped to a consumer can only import what the consumer installed. `@avsbhq/react/testing` also keeps its own, because it serves values through the flag's default variation rather than a rule, so its reads report `source: 'default'` and fire no exposure.\n\n```ts\nimport {\n  createTestClient,\n  createTestDatafile,\n  createTestContext,\n  createMultiTestContext,\n} from '@avsbhq/test';\n\n// A REAL AvsbClient over that datafile: real evaluator, real type checks,\n// real source values. Nothing is fetched, polling is off, logging is silent.\nconst client = createTestClient({ flags: { 'checkout-v2': true } });\nexpect(client.getBoolFlag('checkout-v2', false).isEnabled()).toBe(true);\n\n// The datafile a flags map describes. Each entry becomes a flag served to\n// everyone through a 100% A/B rule, so reads report source: 'rule' and\n// record an exposure, exactly as a fully rolled-out flag does in production.\nconst datafile = createTestDatafile({ 'checkout-v2': true, 'cta-copy': 'Buy now' });\n\n// createTestContext takes PARTIAL CONTEXT FIELDS, not a key string.\nconst ctx = createTestContext({ key: 'u_123', plan: 'pro' });\n// { kind: 'user', key: 'u_123', plan: 'pro' }\n\nconst multiCtx = createMultiTestContext({\n  user: { kind: 'user', key: 'u_123', plan: 'pro' },\n  organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },\n});\n```\n\nFull signatures:\n\n```ts\nfunction createTestClient(options?: {\n  flags?: TestFlags;\n  context?: EvalContext;\n  datafile?: FlagDatafile;\n}): AvsbClient;\n\ntype TestFlagValue = boolean | string | number | object | null;\ntype TestFlags = Record<string, TestFlagValue>;\n\nfunction createTestDatafile(\n  flags?: TestFlags,\n  options?: {\n    sdkKey?: string;\n    environmentKey?: string;\n    /** Move it forward to simulate a republish and wake subscribers. */\n    publishedAt?: string;\n    /** Hand-built entries, for audiences, rollouts, or per-user overrides. */\n    entries?: FlagDatafileEntry[];\n  },\n): FlagDatafile;\n```\n\nTwo details worth knowing:\n\n- The datafile's `collectEndpoint` is `https://collect.test.invalid`. `.invalid` is a reserved TLD that never resolves, so a stray flush in a test cannot reach a real host.\n- `createTestClient` reaches the network at no point, and that includes `await client.onReady()`. `onReady` skips its lazy init once the result has settled, and handing the datafile in settles it. Awaiting readiness is what every framework provider does on mount, so this is the difference between a silent suite and one request per test client.\n\n---\n\n## 9. `Flag<T>` fixtures without a client\n\n```ts\nimport { createFlag, notFoundFlag, notReadyFlag } from '@avsbhq/core';\n\nconst FLAG_ON = createFlag<boolean>({\n  value: true,\n  variationKey: 'on',\n  source: 'rule',\n  ruleId: 'r1',\n  ruleType: 'ab_test',\n  reasons: ['matched audience: paid-users'],\n});\n\nconst MISSING = notFoundFlag<boolean>(false, 'flag not in datafile');\nconst TOO_EARLY = notReadyFlag<boolean>(false, 'SDK not ready');\n```\n\n---\n\n## 10. Migration\n\n### From LaunchDarkly `LDTestData`\n\n| LaunchDarkly                                  | `@avsbhq/test`                                         |\n| --------------------------------------------- | ------------------------------------------------------ |\n| `LDTestData.dataSource()`                     | `createMockClient(flagsFromTestData([...]))`           |\n| `td.flag('key').booleanFlag()`                | `TestData.flag('key').booleanFlag()`                   |\n| `td.flag('key').variationForUser(user, true)` | `TestData.flag('key').variationForUser(userKey, true)` |\n| `td.flag('key').fallthroughVariation(false)`  | same                                                   |\n| `td.build()` passed to init                   | `createMockClient(flagsFromTestData([td.build()]))`    |\n\n### From Statsig local bootstrap\n\n| Statsig                                         | `@avsbhq/test`                               |\n| ----------------------------------------------- | -------------------------------------------- |\n| `new StatsigServer(key, { localMode: true })`   | `createMockServer({ flags: {...} })`         |\n| `override.overrideGate('key', true)`            | `createMockClient({ flags: { key: true } })` |\n| `statsig.logEvent(...)` then `getEventLogQueue` | `client.track(...)` then `client._tracked()` |\n\n### From an earlier @avsbhq/test\n\n| Was                                               | Now                                                     |\n| ------------------------------------------------- | ------------------------------------------------------- |\n| `createMockClient({ flags: [td.build()] })`       | `createMockClient(flagsFromTestData([td.build()]))`     |\n| `<AvsbTestProvider mockFlags={{...}}>`            | `<AvsbTestProvider flags={{...}}>`                      |\n| `@avsbhq/test/jest` preset                        | deleted; see section 1                                  |\n| vitest `plugins: [avsbPlugin()]`                  | `setupAvsbTest()` from `@avsbhq/test/vitest`            |\n| `createTestDatafile({ flags: [entry] })`          | `createTestDatafile({}, { entries: [entry] })`          |\n| `createTestContext('u_123', { plan })`            | `createTestContext({ key: 'u_123', plan })`             |\n| `import { AvsbTestProvider } from '@avsbhq/test'` | `import { AvsbTestProvider } from '@avsbhq/test/react'` |\n\n`createTestDatafile` swapped its parameters (breaking change B34). The first argument is now the flagKey to value MAP, which is what four of the five copies of this function already took and what every example writes; hand-built `FlagDatafileEntry` objects moved to `options.entries`. TypeScript rejects the old call rather than silently building an empty datafile. Reads on a mapped flag now report `source: 'rule'` and record an exposure, where an entry-less datafile reported nothing at all.\n","readmeFilename":"README.md","keywords":["avsb","feature-flags","ab-testing","experiments","testing","mock","vitest","jest"],"license":"MIT"}