---
url: /commerce-klaus/packages/vitest-sfcc.md
---
[![NPM version][npm-image]][npm-url] [![Downloads][npm-downloads-image]][npm-url]

# @commerce-klaus/vitest-sfcc

Cartridge-aware SFCC runtime and dependency mocking for Vitest. It provides a modern alternative to `proxyquire` for statically analyzable cartridge modules.

## TL;DR

```ts{2,5-9} [vitest.config.ts]
import { defineConfig } from "vitest/config"
import sfccVitest from "@commerce-klaus/vitest-sfcc"

export default defineConfig({
  plugins: [
    sfccVitest({
      basePath: "./cartridges",
      cartridgePath: ["app_custom", "app_storefront_base"],
    }),
  ],
})
```

```ts [payment.test.ts]
import { resetSfccRuntime } from "@commerce-klaus/vitest-sfcc"

const runtime = resetSfccRuntime()
runtime.mock("*/cartridge/scripts/payment/provider", providerMock)

const payment = await import("../cartridge/scripts/payment.js")
```

## Shared resolution model

SFCC resolves server-side modules in the context of an ordered **cartridge path**. Entries on the left have higher precedence, so the order is part of application behavior rather than a filesystem detail. See [Cartridges and cartridge paths](/guide/cartridges-and-cartridge-paths) for the central explanation.

* `*/cartridge/...` selects the first matching module in cartridge-path order.
* `~/cartridge/...` selects a module from the importing file's own cartridge.
* `<cartridge>/cartridge/...` selects a module from one named cartridge.
* `module.superModule` selects the next matching implementation after the current cartridge.
* `dw/*` refers to platform APIs and is not resolved from the project filesystem.

::: tip Prefer `~/` for local cartridge dependencies

Use `~/` when a dependency belongs to the importing file's own cartridge. Reserve `*/` for cases
where cartridge-path precedence should deliberately select the effective implementation.

:::

Commerce Klaus shares these rules across its ESLint, TypeScript, Babel, Vite, and Vitest integrations. See [Understanding SFCC module resolution](/guide/sfcc-module-resolution) for the complete model, examples, and edge cases.

Commerce Klaus can read these options from one shared
[`commerce-klaus.config.ts` or `.js` file](/guide/project-configuration).
Package options remain supported and override the central values.

## Why this package exists

SFCC cartridge code uses CommonJS, cartridge-specific module identifiers, platform modules, globals, hooks, controllers, and job metadata that Vitest cannot execute by itself. This package connects the shared cartridge resolver and framework-independent test runtime to Vite's module graph so tests can load real cartridge modules and replace only their external dependencies.

## Choosing the right package

Use this package when cartridge code must run in Vitest. It provides the complete test integration: cartridge resolution, CommonJS transformation, SFCC platform-module fallbacks, globals, dependency mocking, hooks, and execution harnesses.

::: warning Choose one Vite integration

Use [`@commerce-klaus/vitest-sfcc`](/packages/vitest-sfcc/) when cartridge code must run in
Vitest. Use [`@commerce-klaus/vite-plugin-sfcc-modules`](/packages/vite-plugin-sfcc-modules/)
when another Vite-based tool needs cartridge-aware module resolution without the test runtime.
The Vitest integration already includes the required resolution and transformation, so do not
configure both plugins in the same Vitest run.

:::

## Features

* Resolves `dw/*`, `*/`, `~/`, cartridge aliases, relative modules, and `module.superModule`.
* Infers cartridge order from explicit configuration, environment, `jsconfig`, or `site.xml`.
* Derives focused test cartridge paths from central architecture assignments.
* Provides focused SFCC runtime modules, globals, SFRA controller execution, hooks, and job steps.
* Supports specifier mocks and exact resolved-file mocks without `proxyquire`.
* Loads CommonJS cartridge modules through the normal Vite and Vitest pipeline.
* Exposes runtime-only APIs and types through `@commerce-klaus/vitest-sfcc/runtime`.

## Installation

::: code-group

```bash [pnpm]
pnpm add -D @commerce-klaus/vitest-sfcc vitest
```

```bash [npm]
npm install -D @commerce-klaus/vitest-sfcc vitest
```

```bash [Yarn]
yarn add -D @commerce-klaus/vitest-sfcc vitest
```

```bash [Vite+]
vp install -D @commerce-klaus/vitest-sfcc vitest
```

:::

::: info Runtime APIs are included

Do not install `@commerce-klaus/sfcc-test-runtime` separately when using the Vitest integration. It is an internal dependency and its public APIs are re-exported through `@commerce-klaus/vitest-sfcc/runtime`.

:::

## Configuration

```ts
import sfccVitest from "@commerce-klaus/vitest-sfcc"
import { defineConfig } from "vite-plus"

export default defineConfig({
  plugins: [
    sfccVitest({
      basePath: "./cartridges",
      cartridgePath: ["app_custom", "app_storefront_base"],
      runtime: {
        site: { id: "RefArch" },
      },
    }),
  ],
})
```

The cartridge order can also be read from a site template:

```ts{5-7} [vitest.config.ts]
sfccVitest({
  basePath: "./cartridges",
  siteTemplatePath: "./sites/site_template",
  site: "RefArch",
})
```

This reads `sites/<site>/site.xml` below `siteTemplatePath` and uses its `custom-cartridges` value.

### Options

| Option               | Type                                | Required | Description                                                                    |
| -------------------- | ----------------------------------- | -------- | ------------------------------------------------------------------------------ |
| `basePath`           | `string`                            | no       | Cartridge directory override. Defaults to central config, then `cartridges`.   |
| `cartridgePath`      | `string[]`                          | no       | Explicit cartridge order. First match wins.                                    |
| `architectures`      | `CartridgeArchitectureAssignments`  | no       | Architecture assignments overriding the central configuration.                 |
| `cartridgeSelection` | `CartridgeArchitectureSelection`    | no       | Filter the effective path by architecture roles and shared cartridges.         |
| `cwd`                | `string`                            | no       | Working directory used to resolve relative paths. Defaults to `process.cwd()`. |
| `siteTemplatePath`   | `string`                            | no       | Site-template root containing `sites/<site>/site.xml`.                         |
| `site`               | `string`                            | no       | Site identifier used to read `custom-cartridges` from `site.xml`.              |
| `solutionConfigPath` | `string`                            | no       | Path to `cartridges/jsconfig.json` for reference-based cartridge order.        |
| `envCartridgePath`   | `string`                            | no       | Colon-separated cartridge order, matching `SFCC_CARTRIDGE_PATH`.               |
| `configFile`         | `string \| false`                   | no       | Explicit central config path, or `false` to disable discovery.                 |
| `hookDiscovery`      | `false \| { cartridges: string[] }` | no       | Disable automatic hook discovery or limit it to named cartridges.              |
| `runtime`            | `SfccTestRuntimeOptions`            | no       | Initial runtime options, including the current site id and custom preferences. |

Commerce Klaus uses the following precedence when determining cartridge order:

1. An explicit `cartridgePath` option.
2. An `envCartridgePath` option, the Salesforce `SFCC_CARTRIDGES` environment variable, or the legacy `SFCC_CARTRIDGE_PATH` environment variable.
3. References in the configured solution `jsconfig.json`.
4. The site's `custom-cartridges` value in `site.xml`.
5. A deterministic alphabetical filesystem fallback.

The first source that produces entries wins. `SFCC_CARTRIDGES` takes precedence over `SFCC_CARTRIDGE_PATH`, and both comma- and colon-separated values are accepted. Missing cartridge directories are filtered out, and valid results are represented internally as absolute cartridge-root paths.

For reproducible CI and local behavior, prefer an authoritative project source such as an explicit path, solution references, or site metadata. The alphabetical fallback is convenient for initial setup, but it cannot express intentional override precedence.

The same inference rules are shared by the other Commerce Klaus packages through [`@commerce-klaus/sfcc-module-resolver`](../sfcc-module-resolver/).

### Focused cartridge paths

Architecture assignments in `commerce-klaus.config.ts` can define the project
once. The [Cartridge architectures guide](/guide/cartridge-architectures)
defines the shared labels and explains their policy boundaries:

```ts [commerce-klaus.config.ts]
import { defineConfig } from "@commerce-klaus/config"

export default defineConfig({
  cartridgePath: ["app_headless", "bc_custom_api", "app_storefront_base", "modules"],
  architectures: {
    "storefront-next": ["app_headless", "bc_custom_api"],
    sfra: ["app_storefront_base"],
  },
})
```

A Vitest project can then select only the relevant role and explicitly retain
shared infrastructure:

```ts [vitest.config.ts]
sfccVitest({
  cartridgeSelection: {
    architectures: ["storefront-next"],
    cartridges: ["modules"],
  },
})
```

Filtering happens after normal cartridge-path inference and never reorders the
result. By default, cartridges without an architecture assignment are excluded.
Set `includeUnassigned: true` when unassigned integration cartridges should
remain available.

The selected path controls wildcard and alias resolution, `module.superModule`,
hook discovery, and job step definitions together. An empty selection fails
during configuration instead of silently running tests against the wrong
module graph. Use separate Vitest projects when suites require different
architecture selections.

## Runtime API

Application tests can import the common helpers from the package root. Tooling integrations and tests that only need runtime APIs or types can use the lightweight subpath:

```ts
import {
  getSfccRuntime,
  resetSfccRuntime,
  type SfccModule,
} from "@commerce-klaus/vitest-sfcc/runtime"
```

Both entry points address the same active runtime. `resetSfccRuntime()` creates and activates a fresh instance; `getSfccRuntime()` returns the active instance.

## Custom APIs

The runtime provides `dw/system/RESTResponseMgr` and a Custom API harness, so tests do not need to recreate request globals and response spies for every operation:

```ts
import { resetSfccRuntime } from "@commerce-klaus/vitest-sfcc"
import { beforeEach, expect, it, vi } from "vitest"

beforeEach(() => {
  vi.resetModules()
})

it("returns one product", async () => {
  const runtime = resetSfccRuntime()
  const imported = await import("../cartridge/rest-apis/products/getProduct.js")
  const api = imported.default ?? imported

  const result = await runtime.customApi(api).invoke("getProduct", {
    customer: {
      authenticated: true,
      registered: true,
      profile: { customerNo: "00001" },
    },
    headers: { authorization: "Bearer test-token" },
    pathParameters: { productId: "sample-product" },
    queryParameters: { locale: "de-DE" },
  })

  expect(result.response).toMatchObject({
    kind: "success",
    statusCode: 200,
    body: { id: "sample-product" },
  })
})
```

`invoke()` installs `request`, `response`, and `customer` for the operation. The request supports SCAPI path parameters, query parameters through both `httpParameterMap.get()` and named properties, request bodies, and headers. Objects passed through `request`, `response`, or `globals` extend the defaults for project-specific behavior.

Rendered success results expose `kind`, `statusCode`, `body`, headers, and expiration. Rendered RFC 9457 errors additionally expose `type`, `title`, `detail`, and custom attributes. `createSuccess()`, `createEmptySuccess()`, and the supported `createError()` overloads validate their HTTP status ranges. Missing operations, operations that render no response, concurrent invocations, and multiple rendered responses fail explicitly.

The harness executes an already imported operation module. Cartridge discovery and CommonJS loading remain the responsibility of `vitest-sfcc`; the framework-independent runtime does not inspect `rest-apis` directories or enforce OAS contracts. Metadata-loaded operations can opt into focused contract validation as described below.

### Load from project metadata

Use `loadSfccCustomApi()` when a test should follow the effective cartridge path and the checked-in `api.json` registration instead of repeating an implementation path:

```ts
import { loadSfccCustomApi } from "@commerce-klaus/vitest-sfcc"
import { expect, it } from "vitest"

it("returns one product", async () => {
  const api = await loadSfccCustomApi("products", "getProduct")
  const result = await api.invoke({
    pathParameters: { productId: "sample-product" },
    queryParameters: { locale: "de-DE" },
  })

  expect(api.definition.operation).toMatchObject({
    method: "get",
    path: "/products/{productId}",
  })
  expect(result.response.statusCode).toBe(200)
})
```

The plugin resolves Custom API definitions during configuration using the selected cartridge path. For duplicate API and operation IDs, the first cartridge wins. Loading remains lazy: only the selected implementation module is evaluated. Missing APIs, operations, OAS operation declarations, and implementation scripts produce explicit diagnostics with the available IDs where applicable.

Run `sfcc-ts-sync-types` from `@commerce-klaus/typescript-sfcc` to generate the project-specific `SfccCustomApis.Operations` registry. When it is present, `loadSfccCustomApi()` completes and validates operation IDs, and `invoke()` uses the generated request body, path, query, and header parameter types. Projects without generated declarations retain the general string and parameter-record API.

Use `runtime.customApi(importedModule)` when the implementation module itself is the subject of the test or must be supplied directly. Use `loadSfccCustomApi(apiId, operationId)` when the metadata registration, cartridge precedence, OAS operation, and implementation should be tested together.

### Validate the OAS contract

Contract validation is disabled by default so existing tests and direct `runtime.customApi()` calls remain permissive. Enable it for a metadata-loaded operation when the test should also verify its checked-in OAS contract:

```ts
const api = await loadSfccCustomApi("products", "getProduct", {
  validate: true,
})

await api.invoke({
  pathParameters: { productId: "sample-product" },
  queryParameters: { locale: "de-DE" },
})
```

With `validate: true`, `invoke()` validates the request before calling the cartridge module and validates the rendered response afterward. A failure lists every supported contract mismatch with a path such as `request.path.productId` or `response.body.id`. Use `{ validate: { request: true } }` or `{ validate: { response: true } }` to enable only one phase.

The validator covers:

* required path, query, and case-insensitive header parameters and their schemas
* required JSON request bodies and their schemas
* exact, `2XX`-style, and `default` response declarations
* JSON response schemas and responses that declare no body
* local `$ref`, objects, arrays, strings, finite numbers, integers, booleans, `required`, `enum`, `nullable`, properties, items, and `additionalProperties`

::: warning Validation is intentionally partial

Authentication and authorization declarations, non-JSON media types, response headers, formats, composition keywords, and further numeric or string constraints are not evaluated. Unknown local references fail explicitly; unsupported keywords are left to full OAS validation tools rather than approximated.

:::

## Dependency mocking

Vite root-relative cartridge URLs are resolved against the configured Vite root
when loading transformed CommonJS modules. Existing absolute filesystem paths
remain unchanged; Super Module lookup uses the resolved cartridge file.

The optional [SFCC test lint preset](../eslint-config-sfcc/#sfcc-test-patterns)
checks concurrent runtime use, static SFCC imports alongside runtime setup,
runtime mutations in hoisted mock factories, non-absolute exact-file mock paths,
and direct access to SFCC globals through `globalThis` or `global`. It runs in ESLint
and Oxlint without applying cartridge runtime restrictions to test files.

Global checks also cover `vi.stubGlobal()` and static reflection operations for
known SFCC globals. The separate opt-in
[`require-runtime-cleanup` rule](../eslint-config-sfcc/rules/sfcc-test/require-runtime-cleanup.md)
checks for a runtime reset in `afterEach()` in the same suite or an ancestor.
Reset the existing instance with `getSfccRuntime().reset()`; replacing it with
`resetSfccRuntime()` does not restore the previous instance's globals.
Projects with verified central cleanup can declare `{ externalCleanup: true }`.

The preset also warns about SFCC imports directly inside `vi.hoisted()`.
For projects replacing the active runtime in `beforeEach()`, the opt-in
[`no-stale-runtime-reference` rule](../eslint-config-sfcc/rules/sfcc-test/no-stale-runtime-reference.md)
reports test and cleanup callbacks using an instance captured earlier at
file/suite scope. Fetch it inside the callback instead, or assign the newly
created instance to a shared binding in `beforeEach()`.

Test files should load SFCC modules with Vite imports, not native Node.js `require()`.
Use `await import()` after setting up the runtime. Actual `~/` imports require the
test itself to belong to a configured cartridge; a `~/` mock key instead matches
the dependency identifier used by cartridge code and is valid from an external test.
`*/` is an SFCC module prefix, not a glob matching every form of mock identifier.
The test preset checks native cartridge requires and contextless tilde imports.
An additional opt-in extension rule checks file imports and runtime mock IDs,
leaving platform modules such as `dw/system/Site` and `server` extensionless.

The preset's [`no-proxyquire` rule](../eslint-config-sfcc/rules/sfcc-test/no-proxyquire.md)
reports Proxyquire imports and require calls as migration warnings, including chained
`.noCallThru()` loading patterns. Replace dependency maps deliberately; runtime mocks
do not automatically reproduce Proxyquire's call-through, global-stub, or cache options.

Register replacements before dynamically importing the module under test:

```ts
import { getSfccRuntime } from "@commerce-klaus/vitest-sfcc"
import { beforeEach, expect, it, vi } from "vitest"

beforeEach(() => {
  vi.resetModules()
  getSfccRuntime().reset()
})

it("uses the test payment provider", async () => {
  getSfccRuntime().mock("*/cartridge/scripts/payment/provider", {
    authorize: () => ({ authorized: true }),
  })

  const payment = await import("../cartridge/scripts/payment.js")
  expect(payment.default.authorizePayment().authorized).toBe(true)
})
```

Mocks can target `dw/*`, `*/`, `~/`, and cartridge-alias module identifiers. Without a cartridge mock, the real file selected by cartridge resolution is loaded.

Static relative imports are mockable as well, covering a common `proxyquire` pattern:

```ts
getSfccRuntime().mock("./helpers/storeOpeningHours", storeOpeningHoursMock)
const storesHook = await import("../cartridge/scripts/hooks/stores.js")
```

A relative identifier can occur below multiple directories. Use an absolute resolved mock to replace exactly one file:

```ts
getSfccRuntime().mockResolved(
  "/workspace/cartridges/app_custom/cartridge/scripts/helpers/storeOpeningHours.js",
  storeOpeningHoursMock,
)
```

Resolved mocks take precedence over module-identifier mocks. `runtime.reset()` clears both kinds.

## SFCC globals

Cartridge code can access controlled SFCC globals without imports:

```ts
getSfccRuntime().setGlobals({
  customer: { authenticated: true },
  request: { locale: "de_DE", querystring: {} },
  session: { custom: {} },
})
```

The runtime exposes focused `SfccCustomerGlobal`, `SfccRequestGlobal`,
`SfccResponseGlobal`, and `SfccSessionGlobal` contracts. Common properties are
discoverable while project-specific fields remain supported.

Use the inspection API instead of reading or deleting `globalThis` manually:

```ts
const runtime = getSfccRuntime()

expect(runtime.getGlobal("request")?.locale).toBe("de_DE")
expect(runtime.hasGlobal("session")).toBe(true)
expect(runtime.getGlobals()).toMatchObject({
  customer: { authenticated: true },
})

runtime.unsetGlobals("request")
```

Project-specific values can provide their expected type at the call site:

```ts
const organization = runtime.getGlobal<{ ID: string }>("organization")
```

Defaults shared by every test in a Vitest project belong in the plugin's
runtime options:

```ts [vitest.config.ts]
sfccVitest({
  runtime: {
    globals: {
      organization: { ID: "org" },
      session: { custom: { channel: "storefront" } },
    },
  },
})
```

`reset()` restores these configured defaults after removing per-test changes.
`getGlobals()` only includes values managed by the active SFCC runtime;
unrelated Node.js and Vitest globals are intentionally omitted.

The SFCC `empty()` global is installed automatically and covers nullish values, empty strings and arrays, and SFCC collections through `isEmpty()`. It can still be replaced through `setGlobals()` for a specific test.

`runtime.reset()` removes added globals, restores values that existed before the test, and reinstalls the default `empty()` and configured project globals. Use it in `afterEach` as well as test setup to prevent the final test in a worker from leaking context.

## SFRA controllers

The plugin resolves `require("server")` to the runtime's SFRA test server. Import a controller after resetting modules, then execute one of its exported routes:

```ts
const controller = await import("../cartridge/controllers/Checkout.js")
const response = await getSfccRuntime()
  .controller(controller.default)
  .run("Begin", {
    querystring: { stage: "shipping" },
  })

expect(response.view).toBe("checkout/checkout")
expect(response.viewData).toMatchObject({ currentStage: "shipping" })
```

The harness supports `server.get()`, `server.post()`, ordered middleware with `next()`, and response state from `render()`, `json()`, `redirect()`, `setViewData()`, and `getViewData()`. Calls to `setStatusCode()`, `setContentType()`, and `print()` are exposed through `statusCode`, `contentType`, and the ordered `printed` array.

Redirect metadata from `setRedirectStatus()`, `setHttpHeader()`, `cacheExpiration()`, and `log()` is available through the corresponding response state. A pending redirect stops subsequent middleware when the route calls `next()`, matching the SFRA server lifecycle.

Middleware can intentionally stop the chain by omitting `next()`. Passing an `Error` to `next(error)` rejects the Promise returned by `run()`, and calling `next()` repeatedly from one middleware produces an explicit diagnostic.

Controller inheritance works through the same `module.superModule` resolution used by scripts:

```js
const server = require("server")

server.extend(module.superModule)
server.prepend("Show", authorizeCustomer)
server.append("Show", addViewData)
server.replace("Submit", submitReplacement)

module.exports = server.exports()
```

The harness clones inherited routes and preserves the resulting middleware order for assertions and execution.

`module.superModule` loads the next matching implementation in cartridge-path order. Transitive supermodule chains pass through the same CommonJS transformation.

## Job steps

CommonJS job modules use the same cartridge transformation and runtime mocks as controllers and hooks:

The built-in `dw/system/Status` and `dw/system/StatusItem` support message parameters, details, mutable items, property/getter pairs, and multi-item error aggregation.

The built-in `dw/util/ArrayList` supports common construction, mutation, sorting, cloning, and iteration patterns used by cartridge scripts.

The built-in `dw/util/HashMap` supports arbitrary key types, common map operations, bulk copying, cloning, and live collection views.

The built-in `dw/util/StringUtils` supports numbered message formatting and UTF-8 Base64 encoding and decoding. Locale-sensitive formatting remains mockable application behavior.

The built-in `dw/util/Calendar` supports deterministic UTC field operations, date arithmetic, and comparisons. Parsing, locale patterns, and time zones remain mockable application behavior.

```ts
const jobModule = await import("../cartridge/scripts/jobs/GenerateFeed.js")
const jobStep = getSfccRuntime().jobStep(jobModule, {
  context: { feedNumber: 1 },
})

const result = await jobStep.run("Run", {
  TargetFolder: "IMPEX/src/feeds",
})

expect(result).toBe("OK")
expect(jobStep.jobExecution.context.feedNumber).toBe(2)
```

This models task-oriented `script-module-step` functions with the SFCC `(parameters, stepExecution)` signature. `stepExecution.getJobExecution()` returns a stable execution object whose `context` is shared across calls.

Chunk modules are orchestrated with the function names from `steptypes.json`:

```ts
const result = await getSfccRuntime()
  .jobStep(chunkModule)
  .runChunk({
    chunkSize: 1000,
    functions: {
      afterChunk: "afterChunk",
      afterStep: "afterStep",
      beforeStep: "beforeStep",
      getTotalCount: "getTotalCount",
      process: "process",
      read: "read",
      write: "write",
    },
    parameters: { TargetFolder: "IMPEX/src/feeds" },
  })
```

The lifecycle supports asynchronous callbacks, skipped process results, persistent job context, and configurable function names. The write callback receives an iterable SFCC-like list with `size()`, `get()`, `toArray()`, and `isEmpty()`. `afterStep` receives the success state even when another lifecycle function throws.

### Load by type ID

Use `loadSfccJobStep()` to execute the effective `steptypes.json` definition without duplicating its module path, function names, or chunk size in the test:

```ts
import { loadSfccJobStep } from "@commerce-klaus/vitest-sfcc"

const jobStep = await loadSfccJobStep("custom.ExportProducts", {
  context: { exportedFiles: [] },
})

const result = await jobStep.run({
  TargetFolder: "IMPEX/src/feeds",
})

expect(jobStep.definition.kind).toBe("chunk-script-module-step")
expect(jobStep.stepExecution.getStepTypeID()).toBe("custom.ExportProducts")
expect(jobStep.jobExecution.context.exportedFiles).toHaveLength(1)
```

Run `sfcc-ts-sync-types` from `@commerce-klaus/typescript-sfcc` to generate the project-specific `SfccJobSteps.Definitions` registry. When it is present, `loadSfccJobStep()` completes and validates registered type IDs, and `run()` uses the parameters, required fields, defaults, and enum values declared by that step. Projects without the generated registry retain the general string and parameter-record API.

The plugin discovers definitions at configuration time using cartridge-path priority and provides them to every Vitest worker. Loading remains lazy, so unrelated job modules and their platform dependencies are not evaluated. Task definitions invoke their configured `function`; chunk definitions automatically apply their configured lifecycle names and `chunk-size`. Unknown IDs and registrations whose module cannot be resolved produce the same explicit missing-step diagnostic. The harness exposes the loaded type ID through `stepExecution.stepTypeID` and `getStepTypeID()`. Its shared `SfccJobContext` supports both normal property access and common `dw.util.Map` operations, including live key, value, and entry collection views with SFCC-style iterators. Iterators can consume their remaining elements into an independent `SfccList` with parameterless `asList()`. `jobStep.definition` also exposes optional description, site-context, organization-context, parallel-execution, and transactional metadata without inventing a job context or enforcing capabilities that depend on one.

Before invoking the module, `run()` applies declared defaults and `@trim`, then normalizes Boolean, numeric, string, date/time, and `@target-type` values. It also validates declared `enum-values`. This covers SFCC metadata that stores Boolean, numeric, and date defaults as strings. Missing required parameters and invalid converted values reject with a diagnostic containing the type ID and parameter name. Parameters not declared in `steptypes.json` pass through unchanged for test-specific inputs.

If the definition declares `status-codes`, a task's direct result or a chunk step's `afterStep` result is validated when it exposes a string `code` property or `getCode()` value, matching `dw.system.Status`. An undeclared code rejects with the type ID and allowed codes. Definitions without declared status codes remain permissive, as do ordinary data results and `undefined`.

Task definitions with `timeout-in-seconds` reject `run()` when their configured duration elapses. The diagnostic includes the type ID and normalized duration. The harness clears its timeout after either outcome; as with any Promise race, JavaScript already executing in the task cannot be forcibly canceled and may continue its own asynchronous work.

## Hook execution

`dw/system/HookMgr` automatically discovers `hooks.json` through each cartridge's `package.json`. If multiple cartridges register the same extension point, the first resolvable registration in cartridge-path order is used.

```js
const HookMgr = require("dw/system/HookMgr")

if (HookMgr.hasHook("app.payment.authorize")) {
  return HookMgr.callHook("app.payment.authorize", "authorize", paymentId)
}
```

Hook scripts run through the same cartridge transformer and can use `dw/*`, `*/`, `~/`, aliases, and `module.superModule`. `getSfccRuntime().hookCalls` exposes calls for test assertions. Discovered scripts are loaded only when the built-in `HookMgr` is used. An explicit `runtime.mock("dw/system/HookMgr", ...)` takes precedence without evaluating unrelated discovered hook implementations.

Use `loadSfccHook()` when the hook registration itself is part of the test and the implementation path should come from project metadata:

```ts
import { loadSfccHook } from "@commerce-klaus/vitest-sfcc"
import { expect, it } from "vitest"

it("authorizes the payment through the effective hook", async () => {
  const hook = await loadSfccHook("app.payment.authorize")
  const result = hook.call("authorize", "payment-1")

  expect(result).toBe("authorized")
  expect(hook.definition.cartridgeRoot).toContain("int_payment")
  expect(hook.definition.hooksJsonPath).toContain("hooks.json")
})
```

The plugin resolves hooks during configuration, provides the effective registrations to every Vitest worker, and loads only the requested implementation. The returned definition includes the owning cartridge, `hooks.json`, registered script, and resolved implementation path. Unknown hooks list the available extension points, and calling a function not exported by the selected script fails explicitly.

Generated Salesforce hook declarations include an `SfccHooks.Definitions` registry. After `sfcc-ts-sync-types`, known extension points narrow the method name, arguments, and result of `call()`. Project-specific hooks remain open by default and can augment that registry in a project declaration when a typed contract is useful.

Use `dw/system/HookMgr` when testing cartridge code that calls the platform API itself. Use `loadSfccHook(hookName)` when a test should select and invoke the implementation registered by project metadata. Both paths preserve first-cartridge-wins behavior and record invocations in `getSfccRuntime().hookCalls`.

Automatic discovery can be disabled when a focused test does not need the
project's registered hooks:

```ts{5} [vitest.config.ts]
sfccVitest({
  basePath: "./cartridges",
  cartridgePath: ["int_payment", "app_storefront_base"],
  hookDiscovery: false,
})
```

Or restrict discovery to selected cartridges while preserving their order in
the configured cartridge path:

```ts{5} [vitest.config.ts]
sfccVitest({
  basePath: "./cartridges",
  cartridgePath: ["int_payment", "app_storefront_base"],
  hookDiscovery: { cartridges: ["int_payment"] },
})
```

This setting controls both automatic `HookMgr` registration and metadata-aware
loading through `loadSfccHook()`. The runtime's `HookMgr` and hooks registered
directly with `getSfccRuntime().registerHook()` remain available. Plugin options
are evaluated per Vitest project; use a dedicated project configuration when
only a subset of test files needs a different discovery scope.

## CommonJS scope

The transformer supports static bindings such as:

```js
const Transaction = require("dw/system/Transaction")
const provider = require("*/cartridge/scripts/provider")

module.exports = { authorizePayment: authorizePayment }
```

It also supports object destructuring and direct named exports used by hooks, job steps, and legacy controllers:

```js
const { enrichStoreOpeningHours } = require("./helpers/storeOpeningHours")

exports.modifyGETResponse = function (document) {
  enrichStoreOpeningHours(document)
}

module.exports.status = "active"
```

Static literal requires may appear anywhere in an expression, including inside functions:

```js
function getLogger() {
  return require("dw/system/Logger").getLogger("checkout")
}
```

Local `const` aliases initialized with a string literal are also resolved:

```js
const HOOK_MANAGER = "*/cartridge/scripts/hooks/libHookExtMgr"

function getHookManager() {
  return require(HOOK_MANAGER)
}
```

Mutable or computed module IDs and mutations of an existing export property fail with an explicit diagnostic. Supporting additional CommonJS patterns is planned as the transformer matures.

## Relationship to @commerce-klaus/sfcc-test-runtime

`@commerce-klaus/vitest-sfcc` owns cartridge discovery, Vite transformation, virtual modules, hook and job metadata discovery, and the active test lifecycle. [`@commerce-klaus/sfcc-test-runtime`](../sfcc-test-runtime/) is the framework-independent core that provides module mocks, globals, platform modules, and execution harnesses.

Most Vitest projects should install only `@commerce-klaus/vitest-sfcc`. Install the runtime package directly only when building another test-runner integration or using the runtime without Vite and Vitest.

## Development

This repository uses Vite+ (`vp`):

```bash
vp install
vp check
vp test
vp run build
```

## License

MIT

[npm-url]: https://www.npmjs.com/package/@commerce-klaus/vitest-sfcc

[npm-image]: https://badgen.net/npm/v/@commerce-klaus/vitest-sfcc

[npm-downloads-image]: https://badgen.net/npm/dw/@commerce-klaus/vitest-sfcc
