chaijs/chai
github.com/chaijs/chai ↗ · 75 files · 848 KB
chai Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.
Overview
Chai is an ECMAScript-module assertion library for JavaScript tests in Node.js and browsers, offering expect, should, and assert interfaces over a shared assertion engine. Its distinctive implementation combines flag-carrying fluent chains, configurable failure diagnostics, proxy-based typo detection, and plugins that extend or overwrite assertions. At commit d86de2a9519bdd0d89aa64c9fbcc0e880513b153, the published entrypoint is an esbuild-generated bundle; this brief reflects bounded source inspection, with no repository code or tests executed.
System map
The runtime consists of a shared Assertion class, built-in registrations, three public interfaces, and utility functions. Module evaluation installs the core assertions, while consumers explicitly activate plugins or the should prototype accessor. State is held in assertion objects and module-level configuration and registries. Distribution is a generated ESM bundle with separate global-registration modules.
- package.json declares ESM, Node >=18, main ./index.js, and a publish file list containing index.js and register-*.js.
- lib/chai.js imports core/assertions.js for registration side effects and exports Assertion, AssertionError, config, util, expect, assert, should, and Should.
- Assertion owns per-chain __flags, initializes target/message/stack/comparator state, and returns a proxified instance.
- The utility facade delegates deep equality to deep-eql, nested lookup to pathval, error matching to check-error, and value inspection to loupe.
- The inspected state owners are in-memory: config is an exported object, use retains a used-plugin array, and events is a module-level EventTarget.
- build bundles lib/chai.js into index.js as ES2021 ESM with names preserved; prebuild runs clean, whose command deletes index.js and coverage/.
- Lint combines ESLint and Prettier checks over lib; lint:types separately invokes strict, no-emit JavaScript checking through TypeScript.
- Release creation triggers a build/test job followed by a dependent npm publishing job that enables OIDC, applies github.ref_name as the version, and requests public publication with provenance.
Feature catalog
Users choose fluent expect or should assertions, or named assert helpers. Built-ins cover scalar values, object structure, collections, numeric relationships, exceptions, predicates, and callback-driven changes. Modifiers alter comparison depth, property lookup, collection ordering, inclusion, and negation. Failure formatting and plugin installation are additional public capabilities.
- expect(value, message) constructs an Assertion; should()/Should() installs an Object.prototype accessor; assert(expression, message) evaluates truthiness through Assertion.assert. Registration modules expose these styles globally.
- Equality has deliberate interface differences: fluent equal/equals/eq uses === or delegates to eql when deep is set, whereas assert.equal uses == and assert.strictEqual delegates to fluent equal.
- Scalar and shape checks include a/an type matching, truthiness, strict booleans, null/undefined/NaN/existence, numeric Number-or-BigInt detection, callable checks, and extensible/sealed/frozen status.
- Object assertions support direct, own, nested, and deep property comparisons, descriptor inspection, key sets, and respondTo method checks; nested and own cannot be combined.
- Collection and text assertions provide include aliases, empty, length/lengthOf, iterable members with deep/ordered/inclusion modes, oneOf, regular-expression matching, and substring checks; deep inclusion explicitly rejects WeakSet.
- Numeric APIs include above/least/below/most aliases, within bounds, closeTo/approximately tolerance checks, finite-number checks, and assert.operator's explicit comparison-operator switch.
- Behavior assertions synchronously check thrown values against error identity/constructor/message criteria, run satisfy predicates, and measure change/increase/decrease with optional by(delta) checks.
- containSubset recursively matches object fields and array elements, compares Dates by timestamp, and accepts field validators; assert.containsSubset and doesNotContainSubset wrap it. Users can also configure diagnostics and install plugins through use.
Data flow
Assertion input becomes a target reference plus metadata stored in flags. Chain getters and methods propagate those flags, while selected operations replace the target for subsequent assertions. Comparison results pass through a common negation and error-construction boundary. Outputs are continuing assertion objects, explicit return values, or thrown errors; inspected callback operations can also modify consumer-owned state.
- expect forwards its value and optional message to Assertion; construction records object, message, ssfi, lockSsfi, and the current configured deep comparator.
- flag reads or writes __flags according to argument count; transferFlags copies flag values and can exclude object, ssfi, lockSsfi, and message.
- Property and method wrappers call the supplied implementation, preserve explicit results, and otherwise construct another Assertion populated with transferred flags.
- Chainable getters first run chainingBehavior, then create a callable wrapper that inherits assertion properties, copies flags, and is proxified; calling it runs the stored method.
- assertProperty resolves nested paths through getPathInfo or reads obj[name], checks existence and optional equality, then replaces the object flag with the resolved value.
- Assertion.assert sends the expression through util.test for negation, applies showDiff policy, derives message/actual/operator metadata, and throws AssertionError only on failure.
- getMessage selects positive or negative templates, evaluates function templates, replaces this/act/exp placeholders in one pass, and prefixes the custom message; display delegates to loupe and threshold-based summaries.
- change reads a subject before and after invoking the target function, stores delta metadata in flags, and leaves the consumer's resulting state in place; by reads those stored values without invoking the callback again.
Behavior traces
The inspected flows cover module initialization, ordinary and deep equality, property chaining, exception checks, callback changes, and plugin installation. All assertion traces describe source execution order rather than observed runs. The closest equivalent to a persistence flow is propagation of chain state and mutation of consumer-owned objects; these paths do not implement durable storage.
- Startup: importing lib/chai.js evaluates its core/assertions.js dependency; that module registers language getters through Assertion.addProperty, which delegates to addProperty and defines getters on Assertion.prototype.
- Global should startup: register-should.js imports should from the bundle → calls loadShould → defines Object.prototype.should → assigns the returned helpers to globalThis.should; accessor reads unbox wrapper primitives before constructing Assertion.
- Strict equality failure: expect(actual) → Assertion → language getters → equal's methodWrapper → assertEqual's === comparison → Assertion.assert → getMessage → thrown AssertionError with actual/expected/diff metadata.
- Deep equality: .deep sets the deep flag → assertEqual locks ssfi → invokes eql → assertEql calls the comparator stored in the eql flag → common assertion evaluation; construction selected config.deepEqual or util.eql.
- Property continuation: assertProperty validates name/target and rejects nested+own → resolves path or key → checks existence/value → sets object to the property value → methodWrapper transfers that target to the next Assertion.
- Exception flow: assert.throws normalizes shorthand matchers → invokes fluent throw → assertThrows verifies a function and calls it inside try/catch → checks compatible error criteria → stores caughtErr as the target → assert.throws returns that target.
- State-change flow: change validates fn and the subject/property → reads initial → calls fn → reads final → records delta flags → asserts inequality; .by then compares the absolute before/after difference with the requested delta.
- Plugin flow: use builds an exports object → checks used.indexOf(fn) → invokes a new plugin with exports/util → records it only after successful return → returns exports for chaining; addMethod extensions subsequently dispatch registration events.
Testing signals
Tests and CI were inspected, not executed; no passing result or measured coverage is asserted. The configured suites use Mocha for Node and Web Test Runner with Playwright for browsers. Inspected tests exercise interface behavior, error diagnostics, plugin installation, utility registration, configuration, subsets, and cross-realm exceptions. File-selection rules leave some tracked nested tests outside the default suites, and the inventory is bounded rather than exhaustive.
- The inspected scripts define npm test as Node plus browser suites with pretest lint; test-node uses c8 --99 --check-coverage and Mocha with test/bootstrap/index.js over test/*.js. These definitions were not executed.
- globalErr executes a failure callback, optionally rejects Chai implementation stack frames when captureStackTrace is supported, and validates exact messages, regexes, or error-property subsets.
- The three interface suites contain successful assertions and failure checks; expect additionally verifies literal template tags and invalid-property diagnostics, while should installs its prototype interface.
- Plugin tests cover basic, repeated, nested, chained, and detached use calls; utility tests cover target reassignment, selective flag transfer, and addMethod/addProperty/addChainableMethod events.
- Configuration tests back up and restore config per test and inspect proxy enablement, excluded keys, and a custom deepEqual comparator that accepts nearby numbers.
- Subset tests include nested objects, Date mismatch, circular actual objects with finite expected shapes, validator functions, and null/non-object inputs; those examples do not establish behavior for every cyclic expected graph.
- Both default selectors target top-level test/*.js, excluding nested display/type-detect files; browsers additionally exclude virtual-machines.js. That Node-only test uses vm.runInContext to check exceptions from another realm.
- Inspected CI definitions build/test Node 20, 25, and latest, lint on Node 24, and run Chromium/Firefox browser jobs; none of these workflow steps invokes the separate lint:types script.
Risk map
The principal observed boundaries are execution of consumer callbacks and getters, shared prototype/configuration mutation, and assertion-error disclosure. Plugins operate with the consumer process's capabilities. Source inspection also identifies recursive subset matching and exception-sensitive flag restoration as areas for targeted investigation. These are observations or explicitly labeled hypotheses, not confirmed vulnerabilities or runtime failures.
- should intentionally modifies Object.prototype, while registration modules assign globalThis properties; these operations affect the embedding realm beyond a single assertion.
- use directly invokes plugins with shared constructors, utilities, and config; no sandbox or rollback surrounds installation, and the plugin is recorded only after it returns.
- throw, change, satisfy, and subset field validators execute supplied functions synchronously; change retains callback side effects rather than restoring the subject.
- Type detection reads Symbol.toStringTag, property checks read obj[name], and subset comparison reads actual[key]; consumer getters or proxy traps can therefore execute during these reads.
- Failure output includes actual and expected references plus rendered values; truncateThreshold limits display text, but AssertionError metadata still receives the original values.
- Hypothesis: distinct cyclic expected/actual graphs can exhaust recursion in compareSubset because recursive branches have no visited-pair tracking; the inspected circular tests use finite expected structures. (inferred)
- Hypothesis: reusing an Assertion after catching an exception from a deep-equality or overwritten-method call can retain lockSsfi=true, because restoration occurs after the call without finally. (inferred)
- Verification gaps are visible in configuration: default suites omit nested type-detect files, the Deno fixture imports ../index.ts absent from the tracked inventory, and Node CI's matrix does not directly exercise the advertised Node 18 floor.
Extension points
Extensions enter through use and Assertion's registration helpers rather than a router or service registry. New behavior can be implemented as properties, methods, or callable chain properties, with corresponding overwrite mechanisms. Flags carry custom state and registration events expose extension activity. Configuration supplies additional hooks for comparison and diagnostics.
- use(fn) is the plugin entrypoint: callbacks receive the constructed Chai exports object and util, installations are deduplicated by function identity, and returned exports support chained use calls.
- Assertion.addProperty delegates to a configurable prototype getter; the getter can return a custom result or continue the assertion chain automatically.
- Assertion.addMethod wraps arbitrary methods with stack-marker management, continuation, length guards, and proxy diagnostics for uncalled methods.
- Assertion.addChainableMethod separates access-time chainingBehavior from call-time method behavior and stores both in ctx.__methods[name] for later replacement.
- Assertion exposes overwriteProperty, overwriteMethod, and overwriteChainableMethod; overwriteMethod passes the previous implementation to the replacement factory and temporarily locks stack markers.
- util.flag, transferFlags, and Assertion._obj let plugins read/change targets and carry custom metadata; transferFlags can preserve extension flags while excluding four base flags.
- util.events is an EventTarget for registration notifications; method/property events carry name and fn, while chainable-method events also expose chainingBehavior.
- config.deepEqual supplies the comparator captured by new Assertions; useProxy and proxyExcludedKeys alter property diagnostics, while includeStack, showDiff, and truncateThreshold control failure presentation.
Contributor handoff
Begin with the shared engine and the exact public interface involved in the task. Preserve chain continuation, modifier state, target changes, stack filtering, aliases, and diagnostic metadata when editing assertions. Tests and CI were inspected, not executed, so future validation must produce fresh results under explicit execution authorization. The inspected inventory is bounded, and older contribution examples should be reconciled with current scripts.
- Read lib/chai.js, Assertion, the relevant interface, and the exact core registration before changing behavior: the public styles converge on the same engine but may differ in argument normalization or equality semantics.
- Edit tracked lib source with awareness that package consumers and inspected tests import generated index.js; build refreshes that artifact, and prebuild's clean deletes it first.
- Preserve wrapper contracts: undefined results continue via a new Assertion and transferred flags, while explicit results escape the chain; stack-marker changes respect lockSsfi.
- Regression tests should account for target replacement after property, ownPropertyDescriptor, and throw, rather than assuming every chained assertion continues to inspect the initial value.
- Use globalErr when validating failure messages, error metadata, and supported stack filtering; inspect corresponding assert/expect/should tests for public-interface parity.
- Tests and CI were inspected, not executed. When authorized, use the existing Node or browser script for the affected behavior, then lint and broader checks as warranted; lint:types must be invoked separately from lint.
- Restore shared configuration, temporary prototype assertions, and event listeners in tests; existing fixtures demonstrate config restoration and prototype cleanup, but the event fixture's cleanup only removes addMethod/addProperty listeners.
- Prefer plugins for new assertions as CONTRIBUTING recommends, and verify historical command examples against package.json: the guide references make clean and chai.js, while current scripts generate index.js. Do not infer passing tests or complete coverage from this bounded inspection.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 56 | 98% |
| Markdown | 5 | docs/config |
| JSON | 4 | docs/config |
| YAML | 4 | docs/config |
| TypeScript | 1 | 2% |
Start reading here
Probable source entrypoint: lib/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (33 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/chai/ | 32 |
lib/chai.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
lib/ | 33 |
test/ | 20 |
.github/ | 5 |
Docs found: CONTRIBUTING.mdREADME.md
Package
package.json — chai v0.0.0-development, 18 declared dependencies.
@eslint/js@rollup/plugin-commonjs@web/dev-server-rollup@web/test-runner@web/test-runner-playwrightassertion-errorc8check-errordeep-eqlesbuildeslinteslint-plugin-jsdoc
CI & config entrypoints
README.md— Repository readme.github/workflows/node.js.yml— GitHub Actions workflow.github/workflows/npm-publish.yml— GitHub Actions workflow.github/workflows/browsers.yml— GitHub Actions workflowpackage.json— npm scripts: build, clean, format, lint, lint:format, lint:js, lint:types, prebuild
Structural health signals
Average score 8.6/10 across 57 analyzed source files; 6 hotspots surfaced.
test/utilities.js— 1599 lines, score 5 (Large file, Deep nesting)test/expect.js— 2424 lines, score 5.5 (Large file, Deep nesting)test/assert.js— 2447 lines, score 5.5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
d86de2a9519b2026-09-29 — chore(deps): update dependency eslint-plugin-jsdoc to v65 (#1865)3173a3dcb53c2026-09-29 — build(deps-dev): bump ip-address from 10.4.0 to 10.7.2 (#1866)a735838123c22026-09-28 — chore(deps): update dependency prettier to v3.9.9 (#1864)8d42a17db4c02026-09-23 — fix: require callable iterators in iterable assertions (#1861)89c5d8b3f7f42026-09-21 — chore(deps): update dependencies (#1862)