{"id":47748552,"url":"https://github.com/loderunner/chain-mock","last_synced_at":"2026-04-03T02:07:08.897Z","repository":{"id":334938405,"uuid":"1143476929","full_name":"loderunner/chain-mock","owner":"loderunner","description":"Mock fluent/chainable APIs with full call tracking and cross-framework matcher support","archived":false,"fork":false,"pushed_at":"2026-03-31T13:54:53.000Z","size":546,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T15:33:30.638Z","etag":null,"topics":["bun","chainable","fluent","jest","matchers","mock","proxy","testing","vitest"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loderunner.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"ko_fi":"loderunner"}},"created_at":"2026-01-27T16:20:22.000Z","updated_at":"2026-03-31T13:54:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/loderunner/chain-mock","commit_stats":null,"previous_names":["loderunner/chain-mock"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/loderunner/chain-mock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loderunner%2Fchain-mock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loderunner%2Fchain-mock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loderunner%2Fchain-mock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loderunner%2Fchain-mock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loderunner","download_url":"https://codeload.github.com/loderunner/chain-mock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loderunner%2Fchain-mock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31327045,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T01:42:14.489Z","status":"online","status_checked_at":"2026-04-03T02:00:06.642Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","chainable","fluent","jest","matchers","mock","proxy","testing","vitest"],"created_at":"2026-04-03T02:07:04.996Z","updated_at":"2026-04-03T02:07:08.891Z","avatar_url":"https://github.com/loderunner.png","language":"TypeScript","funding_links":["https://ko-fi.com/loderunner"],"categories":[],"sub_categories":[],"readme":"# chain-mock\n\nMock fluent/chainable APIs (Drizzle, Express, D3, Cheerio, ioredis, and more)\nwith full call tracking and cross-framework matcher support.\n\n### The Problem\n\nTesting code that uses chainable/fluent APIs is painful:\n\n```typescript\n// Your code\nconst users = await db\n  .select({ id: users.id })\n  .from(users)\n  .where(eq(users.id, 42));\n\n// Your test... 😱\nvi.mocked(db.select).mockReturnValue({\n  from: vi.fn(() =\u003e ({\n    where: vi.fn().mockResolvedValue([{ id: 42 }]),\n  })),\n});\n```\n\n### The Solution\n\n```typescript\nimport { chainMock, matchers } from 'chain-mock';\n\n// Setup (once in your test setup file)\nexpect.extend(matchers);\n\n// In your test\nconst dbMock = chainMock();\ndbMock.mockResolvedValue([{ id: 42, name: 'Dan' }]);\n\n// Run your code\nconst result = await getUserById(dbMock, 42);\n\n// Assert with ease\nexpect(result).toEqual([{ id: 42, name: 'Dan' }]);\nexpect(dbMock.select.from.where).toHaveBeenChainCalledWith(\n  [{ id: users.id }],\n  [users],\n  [eq(users.id, 42)],\n);\n```\n\n### Table of Contents\n\n- [Installation](#installation)\n- [Framework Setup](#framework-setup)\n- [API Reference](#api-reference)\n- [Custom Matchers](#custom-matchers)\n- [Examples](#examples)\n- [Troubleshooting](#troubleshooting)\n- [License](#license)\n\n## Installation\n\n```bash\nnpm install -D chain-mock\n\n# or yarn\nyarn add -D chain-mock\n\n# or pnpm\npnpm add -D chain-mock\n\n# or bun\nbun add -D chain-mock\n```\n\n## Framework Setup\n\n### Vitest\n\n**1. Register matchers** in your setup file:\n\n```typescript\nimport { expect } from 'vitest';\nimport { matchers } from 'chain-mock';\n\nexpect.extend(matchers);\n```\n\n**2. Add type augmentation** in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"chain-mock/vitest\"]\n  }\n}\n```\n\nOr use a triple-slash reference in a `.d.ts` file:\n\n```typescript\n/// \u003creference types=\"chain-mock/vitest\" /\u003e\n```\n\n### Jest\n\n**1. Register matchers** in your setup file:\n\n```typescript\nimport { matchers } from 'chain-mock';\n\nexpect.extend(matchers);\n```\n\n**2. Add type augmentation** in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"chain-mock/jest\"]\n  }\n}\n```\n\nOr use a triple-slash reference in a `.d.ts` file:\n\n```typescript\n/// \u003creference types=\"chain-mock/jest\" /\u003e\n```\n\n### Bun\n\n**1. Register matchers** in your setup file:\n\n```typescript\nimport { expect } from 'bun:test';\nimport { matchers } from 'chain-mock';\n\nexpect.extend(matchers);\n```\n\n**2. Add type augmentation** in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"bun\", \"chain-mock/bun\"]\n  }\n}\n```\n\nOr use a triple-slash reference in a `.d.ts` file:\n\n```typescript\n/// \u003creference types=\"chain-mock/bun\" /\u003e\n```\n\n\u003e [!WARNING]\n\u003e\n\u003e Bun's `toEqual`, `toBe`, and `toStrictEqual` matchers constrain the expected\n\u003e value to match the received type. When testing `mockReturnValue` results, use\n\u003e an explicit type parameter:\n\u003e\n\u003e ```typescript\n\u003e chain.mockReturnValue('abc123');\n\u003e const result = chain();\n\u003e\n\u003e // Use explicit type parameter to avoid type error\n\u003e expect(result).toEqual\u003cstring\u003e('abc123');\n\u003e ```\n\n### Manual Type Augmentation\n\nIf the built-in type augmentation doesn't work for your setup, you can manually\naugment your framework's types:\n\n```typescript\nimport type { ChainMatchers } from 'chain-mock';\n\n// For Vitest\ndeclare module 'vitest' {\n  interface Assertion\u003cT = any\u003e extends ChainMatchers\u003cT\u003e {}\n  interface AsymmetricMatchersContaining extends ChainMatchers {}\n}\n\n// For Jest with @jest/globals\ndeclare module 'expect' {\n  interface Matchers\u003cR, T\u003e extends ChainMatchers\u003cR\u003e {}\n}\n\n// For Jest with global expect\ndeclare global {\n  namespace jest {\n    interface Matchers\u003cR, T\u003e extends ChainMatchers\u003cR\u003e {}\n  }\n}\n\n// For Bun\ndeclare module 'bun:test' {\n  interface Matchers\u003cT\u003e extends ChainMatchers\u003cT\u003e {}\n  interface AsymmetricMatchers extends ChainMatchers {}\n}\n\n// For other expect-based framework\ndeclare module 'other-expect' {\n  interface Matchers\u003cR\u003e extends ChainMatchers\u003cR\u003e {}\n}\n```\n\n## API Reference\n\n### `chainMock\u003cT\u003e()`\n\nCreates a chainable mock instance.\n\n```typescript\nconst mock = chainMock();\n\n// With type parameter for better inference\nconst mock = chainMock\u003ctypeof db\u003e();\n```\n\n### Mock Configuration\n\nAll configuration methods are chainable and can be set on any path in the chain.\n\n#### Async Values\n\n```typescript\n// Resolve with value when awaited\nmock.mockResolvedValue([{ id: 1 }]);\nmock.mockResolvedValueOnce([{ id: 1 }]);\n\n// Reject with error when awaited\nmock.mockRejectedValue(new Error('Connection failed'));\nmock.mockRejectedValueOnce(new Error('Temporary failure'));\n```\n\n#### Sync Values\n\n```typescript\n// Return value synchronously (breaks the chain)\nmock.digest.mockReturnValue('abc123');\nmock.digest.mockReturnValueOnce('abc123');\n```\n\n#### Custom Implementation\n\n```typescript\n// Full control over behavior\nmock.mockImplementation((...args) =\u003e computeResult(args));\nmock.mockImplementationOnce((...args) =\u003e computeResult(args));\n```\n\n#### Reset and Clear\n\n```typescript\n// Clear call history, keep configured values\nmock.mockClear();\n\n// Reset everything (calls + configured values)\nmock.mockReset();\n```\n\n#### Mock Naming\n\n```typescript\nmock.mockName('dbSelectMock');\nmock.getMockName(); // 'dbSelectMock'\n```\n\n### Direct Call Access\n\nAccess call information directly via the `.mock` property:\n\n```typescript\nmock.select.mock.calls; // [['id'], ['name']]\nmock.select.mock.lastCall; // ['name']\nmock.select.mock.results; // [{ type: 'return', value: ... }]\nmock.select.mock.contexts; // [thisArg1, thisArg2]\nmock.select.mock.invocationCallOrder; // [1, 3]\n```\n\n### Utility Functions\n\n#### `chainMocked\u003cT\u003e(value)`\n\nCasts a value to its `ChainMock` type. Useful for typing mocked imports.\n\n```typescript\nimport { db } from './db';\n\nvi.mock('./db', () =\u003e ({\n  db: chainMock(),\n}));\n\nconst mockDb = chainMocked(db);\nmockDb.select.mockResolvedValue([{ id: 42 }]);\n```\n\n#### `isChainMock(value)`\n\nType guard to check if a value is a `ChainMock` instance.\n\n```typescript\nif (isChainMock(maybeChainMock)) {\n  maybeChainMock.mockReturnValue('test');\n}\n```\n\n#### `clearAllMocks()`\n\nClears call history for all chain mocks. Does not reset configured values.\n\n```typescript\nafterEach(() =\u003e {\n  clearAllMocks();\n});\n```\n\n#### `resetAllMocks()`\n\nResets all chain mocks to their initial state, clearing both call history and\nconfigured values.\n\n```typescript\nafterEach(() =\u003e {\n  resetAllMocks();\n});\n```\n\n## Custom Matchers\n\nAfter calling `expect.extend(matchers)`:\n\n### `toHaveBeenChainCalled()`\n\nVerifies that each segment in the chain was called at least once.\n\n```typescript\nchain.select('id').from('users').where('active');\nexpect(chain.select.from.where).toHaveBeenChainCalled();\n```\n\n### `toHaveBeenChainCalledTimes(n)`\n\nVerifies that each segment in the chain was called exactly `n` times.\n\n```typescript\nchain.select('id').from('users').where('active');\nchain.select('name').from('posts').where('published');\nexpect(chain.select.from.where).toHaveBeenChainCalledTimes(2);\n```\n\n### `toHaveBeenChainCalledWith(...argsPerSegment)`\n\nVerifies that any call to the chain had the corresponding arguments at each\nsegment. Pass one array of arguments per segment.\n\n```typescript\nchain.select('id').from('users').where('active');\nexpect(chain.select.from.where).toHaveBeenChainCalledWith(\n  ['id'],\n  ['users'],\n  ['active'],\n);\n```\n\n### `toHaveBeenChainCalledExactlyOnce()`\n\nVerifies that each segment in the chain was called exactly once.\n\n```typescript\nchain.select('id').from('users').where('active');\nexpect(chain.select.from.where).toHaveBeenChainCalledExactlyOnce();\n```\n\n### `toHaveBeenChainCalledExactlyOnceWith(...argsPerSegment)`\n\nVerifies that each segment was called exactly once with the specified arguments.\n\n```typescript\nchain.select('id').from('users').where('active');\nexpect(chain.select.from.where).toHaveBeenChainCalledExactlyOnceWith(\n  ['id'],\n  ['users'],\n  ['active'],\n);\n```\n\n### `toHaveBeenNthChainCalledWith(n, ...argsPerSegment)`\n\nVerifies that the Nth call to each segment had the corresponding arguments.\n\n```typescript\nchain.select('id').from('users').where('active');\nchain.select('name').from('posts').where('published');\n\nexpect(chain.select.from.where).toHaveBeenNthChainCalledWith(\n  2,\n  ['name'],\n  ['posts'],\n  ['published'],\n);\n```\n\n### `toHaveBeenLastChainCalledWith(...argsPerSegment)`\n\nVerifies that the last call to each segment had the corresponding arguments.\n\n```typescript\nchain.select('id').from('users').where('active');\nchain.select('name').from('posts').where('published');\n\nexpect(chain.select.from.where).toHaveBeenLastChainCalledWith(\n  ['name'],\n  ['posts'],\n  ['published'],\n);\n```\n\n### Callable Root vs. Property Access\n\nThe matchers automatically detect whether the root mock was called as a\nfunction. This is useful for APIs like Cheerio where the root is callable\n(`$('.selector')`).\n\n**When root is called as a function:** The root call is included as the first\nsegment in the assertion. Provide an argument array for it:\n\n```typescript\n// Cheerio-style: root is called as a function\nchain('.product').find('.price').text();\n\n// Root call included - 3 argument arrays for 3 segments\nexpect(chain.find.text).toHaveBeenChainCalledWith(\n  ['.product'], // root call\n  ['.price'], // .find()\n  [], // .text()\n);\n```\n\n**When root is accessed as a property:** The root is not included in the\nassertion. Provide argument arrays only for the accessed segments:\n\n```typescript\n// Drizzle-style: root accessed as property\nchain.select('id').from('users').where('active');\n\n// No root call - 3 argument arrays for 3 segments\nexpect(chain.select.from.where).toHaveBeenChainCalledWith(\n  ['id'], // .select()\n  ['users'], // .from()\n  ['active'], // .where()\n);\n```\n\n## Examples\n\n### Drizzle ORM\n\n\\[ [Full example](examples/drizzle) \\] | \\[\n[Drizzle ORM](https://orm.drizzle.team/) \\]\n\n```typescript\n// Without chain-mock 😱\nvi.mock('./db', () =\u003e ({\n  db: {\n    select: vi.fn(() =\u003e ({\n      from: vi.fn(() =\u003e ({\n        where: vi.fn().mockResolvedValue([{ id: 42, name: 'Dan' }]),\n      })),\n    })),\n  },\n}));\n\nit('finds user by id', async () =\u003e {\n  const result = await findUserById(42);\n  expect(result).toEqual({ id: 42, name: 'Dan' });\n  // No way to easily assert on the chain calls\n});\n```\n\n```typescript\n// With chain-mock ✨\nvi.mock('./db', () =\u003e ({ db: chainMock() }));\n\nconst mockDb = chainMocked(db);\n\nit('finds user by id', async () =\u003e {\n  mockDb.select.from.where.mockResolvedValue([{ id: 42, name: 'Dan' }]);\n\n  const result = await findUserById(42);\n\n  expect(result).toEqual({ id: 42, name: 'Dan' });\n  expect(mockDb.select.from.where).toHaveBeenChainCalledWith(\n    [],\n    [users],\n    [eq(users.id, 42)],\n  );\n});\n```\n\n### Express Response\n\n\\[ [Full example](examples/express) \\] | \\[ [Express](https://expressjs.com/) \\]\n\n```typescript\n// Without chain-mock 😱\nit('returns 404 when user not found', async () =\u003e {\n  const res = { status: vi.fn(() =\u003e res), json: vi.fn(() =\u003e res) };\n\n  await handleGetUser(\n    { params: { id: '999' } } as unknown as Request,\n    res as unknown as Response,\n  );\n\n  expect(res.status).toHaveBeenCalledWith(404);\n  expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });\n  // Assertions are separate - can't verify the chain order\n});\n```\n\n```typescript\n// With chain-mock ✨\nit('returns 404 when user not found', async () =\u003e {\n  const mockRes = chainMock\u003cResponse\u003e();\n\n  await handleGetUser(\n    { params: { id: '999' } } as unknown as Request,\n    mockRes as unknown as Response,\n  );\n\n  expect(mockRes.status.json).toHaveBeenChainCalledWith(\n    [404],\n    [{ error: 'User not found' }],\n  );\n});\n```\n\n### ioredis Pipeline\n\n\\[ [Full example](examples/ioredis) \\] | \\[\n[ioredis](https://github.com/redis/ioredis) \\]\n\n```typescript\n// Without chain-mock 😱\nconst mockExpire = vi.fn(() =\u003e ({ exec: mockExec }));\nconst mockHset = vi.fn(() =\u003e ({ expire: mockExpire }));\n\nvi.mock('./redis', () =\u003e ({ redis: { pipeline: () =\u003e ({ hset: mockHset }) } }));\n\nit('caches session', async () =\u003e {\n  await cacheSession({ userId: '42', token: 'abc' });\n  expect(mockHset).toHaveBeenCalledWith('session:42', 'token', 'abc');\n  expect(mockExpire).toHaveBeenCalledWith('session:42', 3600);\n});\n```\n\n```typescript\n// With chain-mock ✨\nvi.mock('./redis', () =\u003e ({ redis: chainMock() }));\n\nconst mockRedis = chainMocked(redis);\n\nit('caches session', async () =\u003e {\n  await cacheSession({ id: '123', data: { name: 'Dan' } });\n  expect(mockRedis.pipeline.set.expire.exec).toHaveBeenChainCalledWith(\n    [],\n    ['123', JSON.stringify({ name: 'Dan' })],\n    ['123', 3600],\n    [],\n  );\n});\n```\n\n### D3.js\n\n\\[ [Full example](examples/d3) \\] | \\[ [D3.js](https://d3js.org/) \\]\n\n```typescript\n// Without chain-mock 😱\nconst mockAttr2 = vi.fn(() =\u003e mockSelection);\nconst mockAttr = vi.fn(() =\u003e ({ attr: mockAttr2 }));\nconst mockAppend = vi.fn(() =\u003e ({ attr: mockAttr }));\nconst mockEnter = vi.fn(() =\u003e ({ append: mockAppend }));\nconst mockData = vi.fn(() =\u003e ({ enter: mockEnter }));\nconst mockSelectAll = vi.fn(() =\u003e ({ data: mockData }));\nconst mockSelection = { selectAll: mockSelectAll };\nvi.mock('d3', () =\u003e ({ select: () =\u003e mockSelection }));\n// ...and we haven't even written the assertions yet\n```\n\n```typescript\n// With chain-mock ✨\nvi.mock('d3', () =\u003e ({ select: chainMock() }));\n\nconst mockSelect = chainMocked(d3.select);\n\nit('renders bars with correct dimensions', () =\u003e {\n  renderBarChart('#chart', [10, 20, 30]);\n\n  expect(\n    mockSelect.selectAll.data.enter.append.attr.attr,\n  ).toHaveBeenChainCalledWith(\n    ['#chart'],\n    ['.bar'],\n    [[10, 20, 30]],\n    [],\n    ['rect'],\n    ['class', 'bar'],\n    ['height', expect.any(Function)],\n  );\n});\n```\n\n### Cheerio\n\n\\[ [Full example](examples/cheerio) \\] | \\[ [Cheerio](https://cheerio.js.org/)\n\\]\n\n```typescript\n// Without chain-mock 😱\nconst mockText = vi.fn(() =\u003e '$29.99');\nconst mockFirst = vi.fn(() =\u003e ({ text: mockText }));\nconst mockFind = vi.fn(() =\u003e ({ first: mockFirst }));\nconst mock$ = vi.fn(() =\u003e ({ find: mockFind }));\n\nvi.mock('cheerio', () =\u003e ({ load: () =\u003e mock$ }));\n\nit('extracts price', async () =\u003e {\n  const price = await scrapePrice('\u003chtml\u003e...\u003c/html\u003e');\n  expect(mock$).toHaveBeenCalledWith('.product');\n  expect(mockFind).toHaveBeenCalledWith('.price');\n});\n```\n\n```typescript\n// With chain-mock ✨\nconst [mock$] = await vi.hoisted(async () =\u003e {\n  const { chainMock } = await import('chain-mock');\n  return [chainMock\u003ccheerio.CheerioAPI\u003e()];\n});\nvi.mock('cheerio', () =\u003e ({ load: () =\u003e mock$ }));\n\nit('extracts price', async () =\u003e {\n  mock$.find.text.mockReturnValue('$29.99' as any);\n\n  const price = await scrapePrice(`\u003chtml\u003e...\u003c/html\u003e`);\n\n  expect(price).toBe('$29.99');\n  expect(mock$.find.text).toHaveBeenChainCalledWith(\n    ['.product'],\n    ['.price'],\n    [],\n  );\n});\n```\n\n## Troubleshooting\n\n### \"Argument of type 'X' is not assignable to parameter of type 'Y'\" (Bun)\n\nBun's `toEqual`, `toBe`, and `toStrictEqual` matchers use TypeScript's `NoInfer`\nutility to constrain the expected value to match the received type. When a\nChainMock is called, the return type is `ChainMock\u003cT\u003e`, not the underlying value\ntype.\n\n**Solution:** Add an explicit type parameter to the matcher:\n\n```typescript\nconst mock = chainMock();\nmock.mockReturnValue('hello');\nconst result = mock();\n\n// ❌ Error: Argument of type 'string' is not assignable...\nexpect(result).toEqual('hello');\n\n// ✅ Fix: add explicit type parameter\nexpect(result).toEqual\u003cstring\u003e('hello');\n```\n\nSee [Bun Matchers.toEqual](https://bun.sh/docs/test/writing#toequal) for more\ndetails.\n\n### Async function returns `undefined` instead of ChainMock\n\nChainMock implements `PromiseLike`, so when returned from an async function,\nJavaScript automatically awaits it and resolves to its mocked value (or\n`undefined` if no value was configured).\n\n**Solution:** Wrap the ChainMock in a tuple or object to prevent automatic\nresolution.\n\n```typescript\n// Test helper that loads fixtures and creates a configured mock\nasync function setupDbMock() {\n  const fixtures = await loadFixtures('./users.json');\n\n  const mock = chainMock();\n  mock.select.from.where.mockResolvedValue(fixtures);\n\n  return mock; // ❌ Awaited and resolved to undefined!\n}\n\nit('queries users', async () =\u003e {\n  const db = await setupDbMock();\n  db.select('*').from('users'); // ❌ Error: db is undefined\n});\n\n// Fix: wrap in tuple\nasync function setupDbMock() {\n  const fixtures = await loadFixtures('./users.json');\n\n  const mock = chainMock();\n  mock.select.from.where.mockResolvedValue(fixtures);\n\n  return [mock] as const; // ✅ Tuple prevents resolution\n}\n\nit('queries users', async () =\u003e {\n  const [db] = await setupDbMock();\n  db.select('*').from('users'); // ✅ Works!\n});\n```\n\n### \"Cannot access '\\_\\_vi_import_0\\_\\_' before initialization\" (Vitest)\n\n`vi.mock()` is hoisted to the top of the file, before any imports are evaluated.\nIf you try to use `chainMock` from a static import inside `vi.mock()` or\n`vi.hoisted()`, the import hasn't been initialized yet.\n\n**Solution:** Use `vi.hoisted()` with a dynamic `import()` and wrap the mock in\na tuple:\n\n```typescript\n// ❌ Wrong: static import is not available in hoisted code\nimport { chainMock } from 'chain-mock';\nconst mock = vi.hoisted(() =\u003e chainMock()); // Error!\n\n// ✅ Correct: use dynamic import inside vi.hoisted\nconst [mock] = await vi.hoisted(async () =\u003e {\n  const { chainMock } = await import('chain-mock');\n  return [chainMock()];\n});\n\nvi.mock('./module', () =\u003e ({ fn: mock }));\n```\n\nSee [Vitest vi.hoisted](https://vitest.dev/api/vi.html#vi-hoisted) for more\ndetails.\n\n### \"mockClear() on a nested chain path\" error\n\nCalling `mockClear()` on a nested path (e.g.,\n`chain.select.from.where.mockClear()`) throws an error. This is by design:\nnested `mockClear()` would only clear the specified path and its children, not\nancestor paths like `select` or `select.from`. This leads to unexpected behavior\nwhen using chain matchers, which check all segments in the path.\n\n**Solution:** Always call `mockClear()` on the root mock:\n\n```typescript\nconst chain = chainMock();\nchain.select('id').from('users').where('active');\n\n// ❌ Error: clears only \"where\", not \"select\" or \"from\"\nchain.select.from.where.mockClear();\n\n// ✅ Correct: clears all paths in the chain\nchain.mockClear();\n\n// Now assertions work as expected\nchain.select('name').from('posts').where('published');\nexpect(chain.select.from.where).toHaveBeenChainCalledExactlyOnce();\n```\n\nThe same applies to `mockReset()` - always call it on the root mock.\n\n## License\n\nApache-2.0\n\nCopyright 2026 Charles Francoise\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floderunner%2Fchain-mock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floderunner%2Fchain-mock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floderunner%2Fchain-mock/lists"}