From 0cc3c9a83acf373ba0c46fa8d826581343507477 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:01:33 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 14 + README.md | 3 + commands/bupkis.md | 111 ++ plugin.lock.json | 65 + skills/bupkis-assertion-patterns/SKILL.md | 307 ++++ .../references/README.md | 69 + .../references/api_reference.md | 1408 +++++++++++++++++ .../references/common_patterns.md | 603 +++++++ skills/skill-rules.json | 48 + 9 files changed, 2628 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/bupkis.md create mode 100644 plugin.lock.json create mode 100644 skills/bupkis-assertion-patterns/SKILL.md create mode 100644 skills/bupkis-assertion-patterns/references/README.md create mode 100644 skills/bupkis-assertion-patterns/references/api_reference.md create mode 100644 skills/bupkis-assertion-patterns/references/common_patterns.md create mode 100644 skills/skill-rules.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..43c1abf --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "bupkis", + "description": "Idiomatic assertion patterns for the Bupkis testing library", + "version": "1.0.0", + "author": { + "name": "boneskull" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cbfcf8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# bupkis + +Idiomatic assertion patterns for the Bupkis testing library diff --git a/commands/bupkis.md b/commands/bupkis.md new file mode 100644 index 0000000..9fc435a --- /dev/null +++ b/commands/bupkis.md @@ -0,0 +1,111 @@ +--- +description: Get guidance on writing idiomatic bupkis assertions +argument-hint: '[what-to-assert]' +--- + +# Bupkis Assertion Guidance + +Provide guidance on writing idiomatic assertions using the bupkis testing library. + +## Usage + +```bash +/bupkis [optional: what you're trying to assert] +``` + +**Arguments:** + +- `what-to-assert` (optional): Describe what you're trying to test or assert + +## Instructions + +When this command is invoked: + +1. **Load the bupkis assertion patterns skill** from `skills/bupkis-assertion-patterns/SKILL.md` + +2. **If the user provided context**, analyze their needs and: + - Identify which patterns from the skill apply to their situation + - Provide specific code examples using those patterns + - Explain why the recommended approach is better + - Show before/after comparisons from the skill when helpful + +3. **If no context was provided**, offer to help by: + - Asking what they're trying to test or assert + - Listing common scenarios: + - Checking if properties exist on objects + - Verifying object structure and types + - Testing arrays or collections + - Validating promise behavior + - Type checking values + - Offering to show examples of specific patterns + +4. **Always emphasize key principles**: + - Using semantic assertions over indirect checks + - Combining related checks with `to satisfy` + - Choosing positive assertions when possible (e.g., `to be defined` vs `not to be undefined`) + - Letting assertions imply related checks (e.g., `to be an object` implies non-null) + - Using `expectAsync` for promise-based assertions + +## Example Interactions + +**With context:** + +```text +User: /bupkis checking if an object has multiple numeric properties + +Agent: For checking multiple properties with specific types, use `to satisfy` +with nested `expect.it()` assertions: + +✅ DO - concise, shows structure at a glance: +expect(state, 'to satisfy', { + filesCompleted: expect.it('to be a number'), + suitesCompleted: expect.it('to be a number'), + tasksCompleted: expect.it('to be a number'), +}); + +This is better than separate assertions because: +- Shows the expected structure clearly +- Better error messages show exactly which properties failed +- More maintainable - all related checks in one place +``` + +**With specific code:** + +```text +User: /bupkis +How can I improve this? +expect('filesCompleted' in state, 'to be truthy'); + +Agent: Instead of checking property existence with the `in` operator, +use the semantic `to have property` assertion: + +✅ DO - clear, semantic assertion: +expect(state, 'to have property', 'filesCompleted'); + +This is clearer because it directly expresses your intent and provides +better error messages when the property is missing. +``` + +**Without context:** + +```text +User: /bupkis + +Agent: I can help you write idiomatic bupkis assertions! What are you trying to test? + +Common scenarios: +- Checking if properties exist on objects +- Verifying object structure and types +- Testing arrays or collections +- Validating promise behavior +- Type checking values + +You can also paste your current test code and I'll suggest improvements, +or ask about a specific assertion pattern! +``` + +## Related Files + +- [Bupkis Assertion Patterns Skill](../skills/bupkis-assertion-patterns/SKILL.md) +- [API Reference](../skills/bupkis-assertion-patterns/references/api_reference.md) +- [Common Patterns](../skills/bupkis-assertion-patterns/references/common_patterns.md) diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..48a17c1 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,65 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:boneskull/claude-plugins:plugins/bupkis", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "3efc9f78ac8b680e8eb1401eb87b77e34c032eab", + "treeHash": "c6a5230497d739cafe4834d8dcab7d80f7264fb469335b9e1a4dfd667ee484d5", + "generatedAt": "2025-11-28T10:14:18.876593Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "bupkis", + "description": "Idiomatic assertion patterns for the Bupkis testing library", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "65687d92505c34c02de00f67fe2746409c2d4a437153bfb4783ec2cf23ce53c0" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "11a60005cd71663ccbc11c1a249d31ce987dc76b489b7dd406caec0ad3e8db53" + }, + { + "path": "commands/bupkis.md", + "sha256": "8263964b5ad5421efb2ddb29dd1b697b37081dbf385333a04634f3da99db89f4" + }, + { + "path": "skills/skill-rules.json", + "sha256": "01ab72ad5bb5c37b280235b66fdc9f6c04dd015aca474dca0ddfef2a049b5a07" + }, + { + "path": "skills/bupkis-assertion-patterns/SKILL.md", + "sha256": "dd3dd66346e114476b38bbd7a0754c6e1dc1f206b899072c5e5d3947ba147631" + }, + { + "path": "skills/bupkis-assertion-patterns/references/api_reference.md", + "sha256": "65129a484f0edc8097a680b65e0d3f4368120f81b939b1ca509d06a8c6e07a7c" + }, + { + "path": "skills/bupkis-assertion-patterns/references/common_patterns.md", + "sha256": "5154cfb7ebf3b8d52eae37764483a2b8a8409e00b78af7e46d493b24a4daef27" + }, + { + "path": "skills/bupkis-assertion-patterns/references/README.md", + "sha256": "03d7f9aa88fbe29ec820c952b64dd6f70d8f6435aba230f6b649ff207673bd5b" + } + ], + "dirSha256": "c6a5230497d739cafe4834d8dcab7d80f7264fb469335b9e1a4dfd667ee484d5" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/bupkis-assertion-patterns/SKILL.md b/skills/bupkis-assertion-patterns/SKILL.md new file mode 100644 index 0000000..71a5a7e --- /dev/null +++ b/skills/bupkis-assertion-patterns/SKILL.md @@ -0,0 +1,307 @@ +--- +name: bupkis-assertion-patterns +description: How to write idiomatic assertions with the Bupkis assertion library for TypeScript and JavaScript +--- + +# Bupkis Assertion Patterns + +Write idiomatic, expressive assertions using bupkis' powerful assertion vocabulary to make tests more readable and maintainable. + +## When to Use + +- Writing tests with the bupkis assertion library +- Checking properties, types, or structure of objects +- Verifying arrays or collections +- Want clearer, more expressive test failures + +## When NOT to Use + +- When a simple, direct assertion is clearer (don't over-complicate) +- When the pattern doesn't improve readability + +## Core Principles + +1. **Use semantic assertions**: Choose assertions that express intent, not implementation +2. **Combine related checks**: Use `to satisfy` for multiple properties rather than separate assertions +3. **Leverage bupkis vocabulary**: Use built-in assertions like `to have property`, `to be empty`, etc. +4. **Let assertions imply related checks**: e.g., `to be an object` already implies non-null + +## Patterns + +### 1. Property Existence + +**Prefer semantic property checks over truthiness checks.** + +```typescript +// ❌ DON'T - unclear intent, indirect check +expect('filesCompleted' in state, 'to be truthy'); + +// ✅ DO - clear, semantic assertion +expect(state, 'to have property', 'filesCompleted'); +``` + +**Why:** `to have property` expresses the intent clearly and provides better error messages. + +--- + +### 2. Type Checking Multiple Properties + +**Combine related type checks using `to satisfy` instead of separate assertions.** + +```typescript +// ❌ DON'T - repetitive, verbose +expect(typeof state.filesCompleted, 'to equal', 'number'); +expect(typeof state.suitesCompleted, 'to equal', 'number'); +expect(typeof state.tasksCompleted, 'to equal', 'number'); + +// ✅ DO - concise, shows structure at a glance +expect(state, 'to satisfy', { + filesCompleted: expect.it('to be a number'), + suitesCompleted: expect.it('to be a number'), + tasksCompleted: expect.it('to be a number'), +}); +``` + +**Why:** `to satisfy` lets you verify multiple properties in one assertion, showing the expected structure clearly. Better error messages show exactly which properties failed. + +--- + +### 3. Non-Empty Collections + +**Use semantic collection assertions instead of length comparisons.** + +```typescript +// ❌ DON'T - indirect, requires mental math +expect(result.files.length, 'to be greater than', 0); + +// ✅ DO - direct, semantic +expect(result.files, 'not to be empty'); +``` + +**Why:** `not to be empty` directly expresses the intent. Works for arrays, strings, objects, Maps, Sets, etc. + +--- + +### 4. Object and Null Checks + +**Don't redundantly check for null when object check already implies it.** + +```typescript +// ❌ DON'T - redundant null check +expect(config, 'to be an object'); +expect(config, 'not to be null'); + +// ✅ DO - object check implies non-null +expect(config, 'to be an object'); // already implies non-null +``` + +**Why:** In bupkis, `to be an object` already ensures the value is not null. Redundant checks add noise. + +--- + +### 5. Object Structure Verification + +**Verify object structure with `to satisfy` instead of multiple property assertions.** + +```typescript +// ❌ DON'T - fragmented, hard to see expected structure +expect(config, 'to be an object'); +expect(config.iterations, 'to equal', 500); +expect(config.reporters[0], 'to equal', 'json'); +expect(config.reporters.length, 'to equal', 1); + +// ✅ DO - clear, declarative structure check +expect(config, 'to satisfy', { + iterations: 500, + reporters: expect.it('to deep equal', ['json']), +}); +``` + +**Why:** `to satisfy` lets you declaratively specify the expected structure. Combines type check and property validation in one assertion. Shows the expected shape at a glance. + +--- + +### 5b. Multiple Conditions on the Same Result Object + +**Combine multiple checks on a result object with `to satisfy` instead of separate assertions.** + +```typescript +// ❌ DON'T - multiple separate assertions +expect(result.exitCode, 'to equal', 0); +expect(result.stdout, 'to match', /No historical data/); +expect(result.stderr, 'not to match', /toLocaleDateString is not a function/); + +// ✅ DO - single to satisfy assertion +expect(result, 'to satisfy', { + exitCode: 0, + stdout: expect.it('to match', /No historical data/), + stderr: expect.it('not to match', /toLocaleDateString is not a function/), +}); +``` + +**Why:** Groups all related checks on the same object. Shows the expected result state clearly. Easier to maintain - add/remove checks in one place. Better error messages show exactly which property failed. + +--- + +### 6. Defined Value Checks + +**Use positive assertions instead of negated ones when possible.** + +```typescript +// ❌ DON'T - negated assertion +expect(result, 'not to be undefined'); + +// ✅ DO - positive, semantic +expect(result, 'to be defined'); +``` + +**Why:** `to be defined` is clearer and more idiomatic than negating undefined. It's a positive assertion that directly expresses intent. + +--- + +### 7. Chaining Assertions with 'and' + +**Use concatenation when making multiple assertions on the same subject.** + +```typescript +// ❌ DON'T - separate assertions +expect(config, 'to be an object'); // implies non-null +expect(config, 'to have property', 'reporters'); + +// ✅ DO - chain with 'and' +expect(config, 'to be an object', 'and', 'to have property', 'reporters'); +``` + +**Why:** Chaining assertions with `'and'` keeps related checks together in a single statement. More concise and shows that you're checking multiple aspects of the same value. Better error messages that show which part of the chain failed. + +--- + +### 8. Multiple Property Checks with Array + +**Use `to have properties` with an array when checking for multiple properties.** + +```typescript +// ❌ DON'T - chain multiple property checks +expect( + config, + 'to be an object', + 'and', + 'to have property', + 'outputDir', + 'and', + 'to have property', + 'reporters', +); + +// ✅ DO - use 'to have properties' with array +expect(config, 'to have properties', ['outputDir', 'reporters']); +``` + +**Why:** `to have properties` with an array is specifically designed for checking multiple properties at once. More concise than chaining. Shows all required properties clearly in one place. Better error messages that list all missing properties. + +--- + +### 9. Promise Rejection Checks + +**Use `expectAsync` with `'to reject'` for testing promise rejections.** + +```typescript +// ❌ DON'T - wishy-washy try/catch +try { + await configManager.load('nonexistent.config.json'); + expect(true, 'to be truthy'); // Maybe it works? +} catch (error) { + expect(error, 'to be an', Error); // Or maybe it throws? + expect((error as Error).message, 'not to be empty'); +} + +// ✅ DO - explicit promise rejection check +await expectAsync(configManager.load('nonexistent.config.json'), 'to reject'); +``` + +**Why:** Makes the contract explicit - either it should reject or it shouldn't. No ambiguity. `expectAsync` is specifically designed for promise-based assertions. `'to reject'` clearly expresses that rejection is the expected behavior. + +**Related assertions:** + +- `'to reject'` - promise should be rejected +- `'to reject with error satisfying'` - promise should reject with specific error + +--- + +## Advanced `to satisfy` Patterns + +### Partial Object Matching + +```typescript +// Only check specific properties, ignore others +expect(result, 'to satisfy', { + status: 'complete', + // other properties ignored +}); +``` + +### Nested Structure + +```typescript +expect(benchmark, 'to satisfy', { + name: expect.it('to be a string'), + results: expect.it('to satisfy', { + mean: expect.it('to be a number'), + median: expect.it('to be a number'), + }), +}); +``` + +### Array Element Values + +```typescript +// Check specific array values within to satisfy +expect(config, 'to satisfy', { + iterations: 100, + reporters: ['human'], // checks first element matches +}); + +// Or check multiple elements +expect(config, 'to satisfy', { + tags: ['performance', 'critical'], +}); +``` + +### Arrays with Patterns + +```typescript +// All items must satisfy condition +expect(results, 'to have items satisfying', { + duration: expect.it('to be a number'), + status: 'success', +}); +``` + +## Quick Reference + +| Instead of... | Use... | +| -------------------------------------- | ------------------------------------------------------ | +| `'prop' in obj, 'to be truthy'` | `obj, 'to have property', 'prop'` | +| `typeof x.prop, 'to equal', 'number'` | `x, 'to satisfy', {prop: expect.it('to be a number')}` | +| `arr.length, 'to be greater than', 0` | `arr, 'not to be empty'` | +| `result, 'not to be undefined'` | `result, 'to be defined'` | +| Separate expect() on same subject | Chain with `'and'`: `expect(x, 'a', 'and', 'b')` | +| Multiple 'to have property' assertions | `to have properties`, `['prop1', 'prop2']` | +| try/catch for promise rejection | `await expectAsync(promise, 'to reject')` | +| Multiple assertions on the same object | `to satisfy` with object structure | +| Separate object + null checks | Just `to be an object` | + +## Benefits + +1. **Clearer intent**: Semantic assertions express what you're testing, not how +2. **Better error messages**: bupkis shows exactly what failed in structural checks +3. **More maintainable**: Related checks grouped together, easier to update +4. **Less code**: Combine multiple assertions into expressive structure checks +5. **Discover structure**: `to satisfy` shows expected object shape at a glance + +## Tools Used + +- `expect()` with bupkis assertion vocabulary +- `expect.it()` for nested assertions within `to satisfy` +- Semantic assertions: `to have property`, `not to be empty`, `to be an object` +- Structural assertions: `to satisfy`, `to deep equal` diff --git a/skills/bupkis-assertion-patterns/references/README.md b/skills/bupkis-assertion-patterns/references/README.md new file mode 100644 index 0000000..3cbcba1 --- /dev/null +++ b/skills/bupkis-assertion-patterns/references/README.md @@ -0,0 +1,69 @@ +# Bupkis References + +This directory contains reference documentation for the Bupkis assertion library. + +## Available References + +### 1. api_reference.md - Complete API Documentation + +**Comprehensive reference for all built-in Bupkis assertions** organized by category: + +- **Core Concepts** - Natural language assertions, negation, concatenation, embeddable assertions +- **Primitive Assertions** - Type checks (string, number, boolean, null, undefined, etc.) +- **Numeric Assertions** - Number comparisons, ranges, special values (NaN, Infinity) +- **String & Pattern Assertions** - String matching, RegExp, substring operations +- **Collection Assertions** - Arrays, Maps, Sets, WeakMap, WeakSet operations +- **Object Assertions** - Property checks, object matching, `to satisfy` patterns +- **Function Assertions** - Function type checks, arity, throw behavior +- **Equality & Comparison Assertions** - Deep equality, instance checks +- **Error Assertions** - Error type and message validation +- **Date & Time Assertions** - Date comparisons, durations, weekday/weekend checks +- **Promise Assertions** - Async resolution and rejection testing +- **Other Assertions** - Truthy, falsy, defined checks + +**When to use:** When you need to look up the exact syntax for an assertion, understand what parameters it accepts, or see all available aliases. + +### 2. common_patterns.md - Practical Usage Patterns + +**Real-world examples and best practices** for common testing scenarios: + +- **API Response Validation** - REST API, pagination, error responses +- **Configuration Validation** - App config, environment-specific config, feature flags +- **Error Testing Patterns** - Standard errors, custom errors, promise rejections +- **Async Operation Patterns** - Promise resolution, async functions, race conditions +- **Complex Nested Structures** - Deep object validation, arrays of objects, optional properties +- **Test Data Validation** - Factory data, fixtures, mock data consistency +- **Type Safety Patterns** - Discriminated unions, generic validation, branded types +- **Real-World Scenarios** - Database queries, forms, file system, HTTP headers, events + +**When to use:** When implementing tests and need examples of idiomatic Bupkis patterns for common use cases. + +## How These References Are Used + +When Claude loads this skill and you ask about Bupkis assertions, it will: + +1. **Load SKILL.md first** (always in context) - Provides the core patterns and workflow +2. **Load references as needed** - Fetches specific reference files based on your question: + - Questions about "what assertions are available" → `api_reference.md` + - Questions about "how to test API responses" → `common_patterns.md` + - Questions about specific assertion syntax → `api_reference.md` + +This three-level progressive disclosure keeps context usage efficient while ensuring comprehensive information is available when needed. + +## Reference File Organization + +Both reference files use: + +- Clear table of contents for quick navigation +- Consistent formatting (heading hierarchy, code blocks) +- Success ✓ and failure ✗ examples for each assertion +- Inline comments explaining key concepts +- Cross-references between related sections + +## Extending These References + +If you discover new patterns or need to document additional use cases: + +1. **Add patterns to common_patterns.md** - Keep examples practical and well-commented +2. **Update api_reference.md** - If Bupkis adds new assertions +3. **Keep files under 10k words each** - Split into subtopics if growing too large diff --git a/skills/bupkis-assertion-patterns/references/api_reference.md b/skills/bupkis-assertion-patterns/references/api_reference.md new file mode 100644 index 0000000..acfd5ba --- /dev/null +++ b/skills/bupkis-assertion-patterns/references/api_reference.md @@ -0,0 +1,1408 @@ +# Bupkis API Reference + +Complete reference for all built-in Bupkis assertions. This document is organized by assertion category. + +## Table of Contents + +- [Core Concepts](#core-concepts) +- [Primitive Assertions](#primitive-assertions) +- [Numeric Assertions](#numeric-assertions) +- [String & Pattern Assertions](#string--pattern-assertions) +- [Collection Assertions](#collection-assertions) +- [Object Assertions](#object-assertions) +- [Function Assertions](#function-assertions) +- [Equality & Comparison Assertions](#equality--comparison-assertions) +- [Error Assertions](#error-assertions) +- [Date & Time Assertions](#date--time-assertions) +- [Promise Assertions](#promise-assertions) +- [Other Assertions](#other-assertions) + +--- + +## Core Concepts + +### Natural Language Assertions + +Bupkis uses natural language phrases instead of method chaining: + +```typescript +// Instead of: +expect(actual).toEqual(expected); + +// Write: +expect(actual, 'to equal', expected); +expect(actual, 'is', expected); +expect(actual, 'to be', expected); +``` + +### Anatomy of an Assertion + +```typescript +expect(subject, 'phrase', [parameter?, phrase?, parameter?, ...]); +``` + +- **Subject**: The value being tested (first parameter) +- **Phrase**: String describing the assertion (e.g., 'to be', 'to equal') +- **Parameter**: Optional values for comparison or validation + +### Negation + +ANY assertion can be negated by prepending `not` to the first phrase: + +```typescript +expect(42, 'to be', 42); // passes +expect(42, 'not to be', '42'); // passes +expect('hello', 'not to be a number'); // passes +``` + +### Concatenation with 'and' + +Chain multiple assertions on the same subject: + +```typescript +expect( + user, + 'to be an object', + 'and', + 'to have property', + 'name', + 'and', + 'to satisfy', + { age: expect.it('to be greater than', 18) }, +); +``` + +### Embeddable Assertions with expect.it() + +Use `expect.it()` to embed assertions within "to satisfy" patterns: + +```typescript +expect(user, 'to satisfy', { + name: expect.it('to be a string'), + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // RegExp works too + age: expect.it('to be greater than', 18), + roles: [expect.it('to be a string')], // Each element must be a string +}); +``` + +--- + +## Primitive Assertions + +### `{unknown} to be a string` + +**Aliases:** None + +**Checks:** Value is a string primitive or String object + +```typescript +expect('hello', 'to be a string'); // ✓ +expect(42, 'to be a string'); // ✗ +expect(42, 'not to be a string'); // ✓ +``` + +### `{unknown} to be a boolean` + +**Aliases:** `to be a bool`, `to be boolean` + +**Checks:** Value is a boolean primitive or Boolean object + +```typescript +expect(true, 'to be a boolean'); // ✓ +expect(0, 'to be a boolean'); // ✗ +expect(0, 'not to be a boolean'); // ✓ +``` + +### `{unknown} to be a number` + +**Aliases:** `to be finite` + +**Checks:** Value is a finite number (NaN and Infinity excluded) + +```typescript +expect(42, 'to be a number'); // ✓ +expect(NaN, 'to be a number'); // ✗ (NaN not considered a number) +expect(Infinity, 'to be a number'); // ✗ (Infinity not considered a number) +``` + +### `{unknown} to be a bigint` + +**Aliases:** None + +**Checks:** Value is a BigInt + +```typescript +expect(9007199254741991n, 'to be a bigint'); // ✓ +expect(42, 'to be a bigint'); // ✗ +``` + +### `{unknown} to be a symbol` + +**Aliases:** None + +**Checks:** Value is a Symbol + +```typescript +expect(Symbol('foo'), 'to be a symbol'); // ✓ +expect('foo', 'to be a symbol'); // ✗ +``` + +### `{unknown} to be null` + +**Aliases:** None + +**Checks:** Value is exactly null + +```typescript +expect(null, 'to be null'); // ✓ +expect(undefined, 'to be null'); // ✗ +``` + +### `{unknown} to be undefined` + +**Aliases:** None + +**Checks:** Value is exactly undefined + +```typescript +expect(undefined, 'to be undefined'); // ✓ +expect(null, 'to be undefined'); // ✗ +``` + +### `{unknown} to be a primitive` + +**Aliases:** None + +**Checks:** Value is a primitive (string, number, boolean, null, undefined, symbol, bigint) + +```typescript +expect('hello', 'to be a primitive'); // ✓ +expect(42, 'to be a primitive'); // ✓ +expect({}, 'to be a primitive'); // ✗ +``` + +--- + +## Numeric Assertions + +### `{unknown} to be infinite` + +**Aliases:** None + +**Checks:** Value is Infinity or -Infinity + +```typescript +expect(Infinity, 'to be infinite'); // ✓ +expect(-Infinity, 'to be infinite'); // ✓ +expect(42, 'to be infinite'); // ✗ +``` + +### `{unknown} to be Infinity` + +**Aliases:** None + +**Checks:** Value is exactly positive Infinity + +```typescript +expect(Infinity, 'to be Infinity'); // ✓ +expect(-Infinity, 'to be Infinity'); // ✗ +``` + +### `{unknown} to be -Infinity` + +**Aliases:** None + +**Checks:** Value is exactly negative Infinity + +```typescript +expect(-Infinity, 'to be -Infinity'); // ✓ +expect(Infinity, 'to be -Infinity'); // ✗ +``` + +### `{unknown} to be positive` + +**Aliases:** `to be a positive number` + +**Checks:** Value is a positive finite number (excludes 0) + +```typescript +expect(42, 'to be positive'); // ✓ +expect(0, 'to be positive'); // ✗ (zero is not positive) +expect(-5, 'to be positive'); // ✗ +``` + +### `{unknown} to be a positive integer` + +**Aliases:** `to be a positive int` + +**Checks:** Value is a positive integer (excludes 0) + +```typescript +expect(42, 'to be a positive integer'); // ✓ +expect(3.14, 'to be a positive integer'); // ✗ (not an integer) +expect(0, 'to be a positive integer'); // ✗ (zero excluded) +``` + +### `{unknown} to be negative` + +**Aliases:** `to be a negative number` + +**Checks:** Value is a negative finite number (excludes 0) + +```typescript +expect(-42, 'to be negative'); // ✓ +expect(0, 'to be negative'); // ✗ +expect(5, 'to be negative'); // ✗ +``` + +### `{unknown} to be a negative integer` + +**Aliases:** `to be a negative int` + +**Checks:** Value is a negative integer (excludes 0) + +```typescript +expect(-42, 'to be a negative integer'); // ✓ +expect(-3.14, 'to be a negative integer'); // ✗ (not an integer) +``` + +### `{unknown} to be NaN` + +**Aliases:** None + +**Checks:** Value is NaN + +```typescript +expect(NaN, 'to be NaN'); // ✓ +expect(42, 'to be NaN'); // ✗ +``` + +### `{unknown} to be an integer` + +**Aliases:** `to be a safe integer`, `to be an int`, `to be a safe int` + +**Checks:** Value is a safe integer + +```typescript +expect(42, 'to be an integer'); // ✓ +expect(3.14, 'to be an integer'); // ✗ +``` + +### `{unknown} to be greater than {number}` + +**Aliases:** None + +**Checks:** Value is strictly greater than the parameter + +```typescript +expect(10, 'to be greater than', 5); // ✓ +expect(5, 'to be greater than', 10); // ✗ +``` + +### `{unknown} to be less than {number}` + +**Aliases:** `to be lt` + +**Checks:** Value is strictly less than the parameter + +```typescript +expect(5, 'to be less than', 10); // ✓ +expect(10, 'to be less than', 5); // ✗ +``` + +### `{unknown} to be greater than or equal to {number}` + +**Aliases:** `to be at least`, `to be gte` + +**Checks:** Value is greater than or equal to the parameter + +```typescript +expect(10, 'to be at least', 10); // ✓ +expect(15, 'to be at least', 10); // ✓ +expect(5, 'to be at least', 10); // ✗ +``` + +### `{unknown} to be less than or equal to {number}` + +**Aliases:** `to be at most`, `to be lte` + +**Checks:** Value is less than or equal to the parameter + +```typescript +expect(10, 'to be at most', 10); // ✓ +expect(5, 'to be at most', 10); // ✓ +expect(15, 'to be at most', 10); // ✗ +``` + +### `{number} to be between {number} and {number}` + +**Aliases:** `to be within` + +**Checks:** Value is between two numbers (inclusive) + +```typescript +expect(5, 'to be between', 1, 10); // ✓ +expect(15, 'to be between', 1, 10); // ✗ +``` + +### `{number} to be close to {number} within {number}` + +**Aliases:** None + +**Checks:** Value is within tolerance of target + +**Parameters:** (subject, target, tolerance) + +```typescript +expect(1.0, 'to be close to', 1.1, 0.2); // ✓ (diff = 0.1) +expect(3.14159, 'to be close to', 3.14, 0.01); // ✓ +expect(1.0, 'to be close to', 2.0, 0.5); // ✗ (diff = 1.0) +``` + +--- + +## String & Pattern Assertions + +### `{string} to begin with {string}` + +**Aliases:** `to start with` + +**Checks:** String starts with the specified prefix + +```typescript +expect('hello world', 'to begin with', 'hello'); // ✓ +expect('hello world', 'to start with', 'world'); // ✗ +``` + +### `{string} to end with {string}` + +**Aliases:** None + +**Checks:** String ends with the specified suffix + +```typescript +expect('hello world', 'to end with', 'world'); // ✓ +expect('hello world', 'to end with', 'hello'); // ✗ +``` + +### `{string} to match {RegExp}` + +**Aliases:** None + +**Checks:** String matches the regular expression + +```typescript +expect('hello123', 'to match', /\d+/); // ✓ +expect('JavaScript', 'to match', /^Java/); // ✓ +expect('hello', 'to match', /\d+/); // ✗ +``` + +### `{string} to be empty` + +**Aliases:** None + +**Checks:** String has zero length + +```typescript +expect('', 'to be empty'); // ✓ +expect('hello', 'to be empty'); // ✗ +``` + +### `{string} to be non-empty` + +**Aliases:** None + +**Checks:** String has at least one character + +```typescript +expect('hello', 'to be non-empty'); // ✓ +expect(' ', 'to be non-empty'); // ✓ (whitespace counts) +expect('', 'to be non-empty'); // ✗ +``` + +### `{string} includes {string}` + +**Aliases:** `contains` + +**Checks:** String contains the substring + +```typescript +expect('hello world', 'includes', 'world'); // ✓ +expect('hello', 'includes', 'world'); // ✗ +``` + +### `{unknown} to be a RegExp` + +**Aliases:** `to be a regex`, `to be a regexp` + +**Checks:** Value is a RegExp object + +```typescript +expect(/hello/, 'to be a RegExp'); // ✓ +expect(new RegExp('world'), 'to be a regex'); // ✓ +expect('hello', 'to be a RegExp'); // ✗ +``` + +--- + +## Collection Assertions + +### `{unknown} to be an array` + +**Aliases:** `to be array` + +**Checks:** Value is an Array + +```typescript +expect([], 'to be an array'); // ✓ +expect([1, 2, 3], 'to be an array'); // ✓ +expect('hello', 'to be an array'); // ✗ +``` + +### `{arraylike} to be empty` + +**Aliases:** None + +**Checks:** Array/string/collection has zero length + +```typescript +expect([], 'to be empty'); // ✓ +expect([1, 2, 3], 'to be empty'); // ✗ +``` + +### `{array} to have length {nonnegative-integer}` + +**Aliases:** `to have size` + +**Checks:** Array/string has exact length + +```typescript +expect([1, 2, 3], 'to have length', 3); // ✓ +expect('hello', 'to have length', 5); // ✓ +expect([1, 2], 'to have length', 3); // ✗ +``` + +### `{arraylike} to be non-empty` + +**Aliases:** None + +**Checks:** Array/string has at least one element + +```typescript +expect([1, 2, 3], 'to be non-empty'); // ✓ +expect([], 'to be non-empty'); // ✗ +``` + +### `{array} to contain {any}` + +**Aliases:** `to include` + +**Checks:** Array contains the value + +```typescript +expect([1, 2, 3], 'to contain', 2); // ✓ +expect(['a', 'b'], 'to include', 'c'); // ✗ +``` + +### `{Map} to contain {any}` + +**Aliases:** `to include` + +**Checks:** Map contains the key + +```typescript +const map = new Map([['key1', 'value1']]); +expect(map, 'to contain', 'key1'); // ✓ +expect(map, 'to contain', 'key2'); // ✗ +``` + +### `{Map} to have size {nonnegative-integer}` + +**Aliases:** None + +**Checks:** Map has exact size + +```typescript +const map = new Map([ + ['a', 1], + ['b', 2], +]); +expect(map, 'to have size', 2); // ✓ +expect(map, 'to have size', 3); // ✗ +``` + +### `{Map} to be empty` + +**Aliases:** None + +**Checks:** Map has no entries + +```typescript +expect(new Map(), 'to be empty'); // ✓ +expect(new Map([['a', 1]]), 'to be empty'); // ✗ +``` + +### `{Set} to contain {any}` + +**Aliases:** `to include` + +**Checks:** Set contains the value + +```typescript +const set = new Set([1, 2, 3]); +expect(set, 'to contain', 2); // ✓ +expect(set, 'to contain', 5); // ✗ +``` + +### `{Set} to have size {nonnegative-integer}` + +**Aliases:** None + +**Checks:** Set has exact size + +```typescript +const set = new Set([1, 2, 3]); +expect(set, 'to have size', 3); // ✓ +expect(set, 'to have size', 5); // ✗ +``` + +### `{Set} to be empty` + +**Aliases:** None + +**Checks:** Set has no elements + +```typescript +expect(new Set(), 'to be empty'); // ✓ +expect(new Set([1]), 'to be empty'); // ✗ +``` + +### `{unknown} to be a Set` + +**Aliases:** None + +**Checks:** Value is a Set + +```typescript +expect(new Set(), 'to be a Set'); // ✓ +expect([1, 2, 3], 'to be a Set'); // ✗ +``` + +### `{WeakMap} to contain {object | symbol}` + +**Aliases:** `to include` + +**Checks:** WeakMap contains the key + +```typescript +const obj = {}; +const wm = new WeakMap([[obj, 'value']]); +expect(wm, 'to contain', obj); // ✓ +``` + +### `{unknown} to be a WeakMap` + +**Aliases:** None + +**Checks:** Value is a WeakMap + +```typescript +expect(new WeakMap(), 'to be a WeakMap'); // ✓ +expect(new Map(), 'to be a WeakMap'); // ✗ +``` + +### `{WeakSet} to contain {object | symbol}` + +**Aliases:** `to include` + +**Checks:** WeakSet contains the value + +```typescript +const obj = {}; +const ws = new WeakSet([obj]); +expect(ws, 'to contain', obj); // ✓ +``` + +### `{unknown} to be a WeakSet` + +**Aliases:** None + +**Checks:** Value is a WeakSet + +```typescript +expect(new WeakSet(), 'to be a WeakSet'); // ✓ +expect(new Set(), 'to be a WeakSet'); // ✗ +``` + +--- + +## Object Assertions + +### `{unknown} to be an object` + +**Aliases:** None + +**Checks:** Value is an object (includes arrays, excludes null) + +```typescript +expect({}, 'to be an object'); // ✓ +expect([], 'to be an object'); // ✓ (arrays are objects) +expect(new Date(), 'to be an object'); // ✓ +expect(null, 'to be an object'); // ✗ +expect('hello', 'to be an object'); // ✗ +``` + +**Note:** `to be an object` already implies non-null. No need for redundant null checks. + +### `{unknown} to be a record` + +**Aliases:** `to be a plain object` + +**Checks:** Value is a plain object (excludes arrays, dates, etc.) + +```typescript +expect({}, 'to be a record'); // ✓ +expect({ a: 1 }, 'to be a plain object'); // ✓ +expect([], 'to be a record'); // ✗ +expect(new Date(), 'to be a record'); // ✗ +``` + +### `{object} to be empty` + +**Aliases:** None + +**Checks:** Object has no own enumerable properties + +```typescript +expect({}, 'to be empty'); // ✓ +expect({ a: 1 }, 'to be empty'); // ✗ +``` + +### `{object} to have keys {array}` + +**Aliases:** `to have properties`, `to have props`, `to include keys`, `to include properties`, `to include props`, `to contain keys`, `to contain properties`, `to contain props` + +**Checks:** Object has all specified keys + +```typescript +expect({ a: 1, b: 2 }, 'to have keys', ['a', 'b']); // ✓ +expect({ a: 1 }, 'to have keys', ['a', 'b']); // ✗ (missing 'b') +``` + +### `{object} to have key {keypath}` + +**Aliases:** `to have property`, `to have prop`, `to include key`, `to include property`, `to include prop`, `to contain key`, `to contain property`, `to contain prop` + +**Checks:** Object has property at keypath (supports dot/bracket notation) + +**Keypath formats:** + +- Dot notation: `'prop.nested'` +- Bracket notation: `'arr[0]'`, `'obj["key"]'` +- Mixed: `'data.items[1].name'` + +```typescript +const obj = { + foo: { bar: [{ baz: 'value' }] }, + items: [{ id: 1 }, { id: 2 }], +}; + +expect(obj, 'to have property', 'foo.bar'); // ✓ +expect(obj, 'to have key', 'foo.bar[0].baz'); // ✓ +expect(obj, 'to have property', 'items[1].id'); // ✓ +expect(obj, 'to have key', 'nonexistent.path'); // ✗ +``` + +### `{object} to have exact key {string | number | symbol}` + +**Aliases:** `to have exact property`, `to have exact prop` + +**Checks:** Object has direct property (no keypath traversal) + +```typescript +const sym = Symbol('test'); +const obj = { + 'key.with.dots': 'value', // literal key, not nested + 'key[with]brackets': 'value', // literal key, not array + [sym]: 'symbol value', +}; + +expect(obj, 'to have exact key', 'key.with.dots'); // ✓ (literal key) +expect(obj, 'to have exact property', 'key[with]brackets'); // ✓ +expect(obj, 'to have exact key', sym); // ✓ +``` + +### `{object} to satisfy {any}` + +**Aliases:** `to be like` + +**Checks:** Object contains at least the specified properties (partial match) + +**Special features:** + +- RegExp values test corresponding property as string +- `expect.it()` embeds assertions +- Ignores extra properties + +```typescript +expect({ a: 1, b: 2, c: 3 }, 'to satisfy', { a: 1, b: 2 }); // ✓ + +// Using RegExp +expect({ email: 'user@example.com', phone: '+1-555-0123' }, 'to satisfy', { + email: /^user@/, + phone: /^\+1-555/, +}); // ✓ + +// Using expect.it() +expect({ name: 'John', age: 30 }, 'to satisfy', { + name: expect.it('to be a string'), + age: expect.it('to be greater than', 18), +}); // ✓ +``` + +### `{object} to have a null prototype` + +**Aliases:** `to be a dictionary` + +**Checks:** Object was created with Object.create(null) + +```typescript +const obj = Object.create(null); +expect(obj, 'to have a null prototype'); // ✓ +expect({}, 'to have a null prototype'); // ✗ +``` + +### `{string | number | symbol} to be an enumerable property of {non-null}` + +**Aliases:** `{non-null} to have enumerable property {string | number | symbol}` + +**Checks:** Property exists and is enumerable + +```typescript +const obj = { a: 1, b: 2 }; +expect('a', 'to be an enumerable property of', obj); // ✓ + +Object.defineProperty(obj, 'c', { value: 3, enumerable: false }); +expect('c', 'to be an enumerable property of', obj); // ✗ +``` + +### `{unknown} to be sealed` + +**Aliases:** None + +**Checks:** Object is sealed (Object.seal()) + +```typescript +const obj = { a: 1 }; +Object.seal(obj); +expect(obj, 'to be sealed'); // ✓ +expect({}, 'to be sealed'); // ✗ +``` + +### `{unknown} to be frozen` + +**Aliases:** None + +**Checks:** Object is frozen (Object.freeze()) + +```typescript +const obj = { a: 1 }; +Object.freeze(obj); +expect(obj, 'to be frozen'); // ✓ +expect({}, 'to be frozen'); // ✗ +``` + +### `{unknown} to be extensible` + +**Aliases:** None + +**Checks:** New properties can be added to object + +```typescript +expect({}, 'to be extensible'); // ✓ + +const obj = {}; +Object.preventExtensions(obj); +expect(obj, 'to be extensible'); // ✗ +``` + +--- + +## Function Assertions + +### `{unknown} to be a function` + +**Aliases:** None + +**Checks:** Value is a function + +```typescript +expect(() => {}, 'to be a function'); // ✓ +expect(Math.max, 'to be a function'); // ✓ +expect('hello', 'to be a function'); // ✗ +``` + +### `{unknown} to be an async function` + +**Aliases:** None + +**Checks:** Value is an async function + +```typescript +expect(async () => {}, 'to be an async function'); // ✓ +expect(() => {}, 'to be an async function'); // ✗ +``` + +### `{unknown} to be a constructor` + +**Aliases:** `to be constructible`, `to be a class` + +**Checks:** Function can be called with 'new' + +**Warning:** Cannot reliably distinguish classes from regular functions + +```typescript +class MyClass {} +expect(MyClass, 'to be a class'); // ✓ +expect(Date, 'to be a constructor'); // ✓ +``` + +### `{function} to have arity {nonnegative-integer}` + +**Aliases:** None + +**Checks:** Function has exact number of parameters + +```typescript +function add(a, b) { + return a + b; +} +expect(add, 'to have arity', 2); // ✓ +expect(add, 'to have arity', 3); // ✗ +``` + +### `{function} to throw [{any}]` + +**Aliases:** `to throw an error satisfying` + +**Checks:** Function throws when called + +**Optional parameter:** Uses "to satisfy" semantics (string/RegExp/object matching) + +```typescript +expect(() => { + throw new Error('oops'); +}, 'to throw'); // ✓ + +// String matching +expect( + () => { + throw new Error('Specific error'); + }, + 'to throw', + 'Specific error', +); // ✓ + +// RegExp matching +expect( + () => { + throw new Error('Error: failed'); + }, + 'to throw', + /failed/, +); // ✓ + +// Object matching +expect( + () => { + throw new Error('Custom error'); + }, + 'to throw', + { message: 'Custom error' }, +); // ✓ +``` + +### `{function} to throw a {constructor}` + +**Aliases:** `to throw an` + +**Checks:** Function throws specific error type + +```typescript +expect( + () => { + throw new TypeError('type error'); + }, + 'to throw a', + TypeError, +); // ✓ + +expect( + () => { + throw new TypeError('type error'); + }, + 'to throw a', + RangeError, +); // ✗ +``` + +### `{function} to throw a {constructor} satisfying {any}` + +**Aliases:** `to throw an {constructor} satisfying` + +**Checks:** Function throws specific error type matching pattern + +```typescript +expect( + () => { + const err = new Error('Custom error'); + err.code = 'CUSTOM_CODE'; + throw err; + }, + 'to throw a', + Error, + 'satisfying', + { code: 'CUSTOM_CODE' }, +); // ✓ +``` + +--- + +## Equality & Comparison Assertions + +### `{unknown} to equal {any}` + +**Aliases:** `to be`, `equals`, `is`, `is equal to`, `to strictly equal`, `is strictly equal to` + +**Checks:** Values are strictly equal (===) + +```typescript +expect(42, 'to be', 42); // ✓ +expect('hello', 'to equal', 'hello'); // ✓ +expect(42, 'is', '42'); // ✗ (different types) +expect({}, 'to equal', {}); // ✗ (different references) +``` + +### `{unknown} to deep equal {any}` + +**Aliases:** `to deeply equal` + +**Checks:** Values are deeply equal (recursive comparison) + +```typescript +expect({ a: 1, b: 2 }, 'to deep equal', { a: 1, b: 2 }); // ✓ +expect([1, 2, 3], 'to deeply equal', [1, 2, 3]); // ✓ +expect({ a: 1 }, 'to deep equal', { a: 1, b: 2 }); // ✗ +``` + +### `{unknown} to be one of {array}` + +**Aliases:** None + +**Checks:** Value is in the array + +```typescript +expect(2, 'to be one of', [1, 2, 3]); // ✓ +expect('blue', 'to be one of', ['red', 'green', 'blue']); // ✓ +expect(5, 'to be one of', [1, 2, 3]); // ✗ +``` + +### `{unknown} to be an instance of {constructor}` + +**Aliases:** `to be a`, `to be an` + +**Checks:** Value is instance of constructor + +```typescript +expect(new Date(), 'to be an instance of', Date); // ✓ +expect([], 'to be a', Array); // ✓ +expect('hello', 'to be an instance of', Number); // ✗ +``` + +### `{unknown} to be a {intrinsic-type}` + +**Aliases:** `to be an`, `to have type` + +**Checks:** Value is of intrinsic type (case-insensitive) + +**Intrinsic types:** string, number, boolean, bigint, symbol, undefined, object, function, null, Map, Set, WeakMap, WeakSet, WeakRef, Date, Error, Array, RegExp, Promise + +```typescript +expect(new Date(), 'to be a', 'Date'); // ✓ +expect([], 'to be an', 'Array'); // ✓ +expect(1, 'to be a', 'number'); // ✓ +``` + +--- + +## Error Assertions + +### `{unknown} to be an Error` + +**Aliases:** `to be a Error` + +**Checks:** Value is an Error instance + +```typescript +expect(new Error(), 'to be an Error'); // ✓ +expect(new TypeError(), 'to be an Error'); // ✓ +expect('error message', 'to be an Error'); // ✗ +``` + +### `{Error} to have message {string}` + +**Aliases:** None + +**Checks:** Error has exact message + +```typescript +const error = new Error('Something went wrong'); +expect(error, 'to have message', 'Something went wrong'); // ✓ +expect(error, 'to have message', 'Different message'); // ✗ +``` + +### `{Error} to have message matching {RegExp}` + +**Aliases:** None + +**Checks:** Error message matches pattern + +```typescript +const error = new Error('File not found: /path/to/file.txt'); +expect(error, 'to have message matching', /File not found/); // ✓ +expect(error, 'to have message matching', /\.txt$/); // ✓ +``` + +--- + +## Date & Time Assertions + +### `{unknown} to be a Date` + +**Aliases:** `to be a date` + +**Checks:** Value is a Date object + +```typescript +expect(new Date(), 'to be a Date'); // ✓ +expect('2024-01-01', 'to be a Date'); // ✗ +expect(1704067200000, 'to be a Date'); // ✗ (timestamp) +``` + +### `{unknown} to be a valid date` + +**Aliases:** `to be date-like` + +**Checks:** Value can be converted to a valid date + +```typescript +expect(new Date(), 'to be a valid date'); // ✓ +expect('2024-01-01', 'to be date-like'); // ✓ +expect(1704067200000, 'to be a valid date'); // ✓ (timestamp) +expect('invalid-date', 'to be a valid date'); // ✗ +``` + +### `{date-like} to be before {date-like}` + +**Aliases:** None + +**Checks:** Date is before another date + +```typescript +expect(new Date('2022-01-01'), 'to be before', new Date('2023-01-01')); // ✓ +expect('2022-01-01', 'to be before', '2023-01-01'); // ✓ +``` + +### `{date-like} to be after {date-like}` + +**Aliases:** None + +**Checks:** Date is after another date + +```typescript +expect(new Date('2023-01-01'), 'to be after', new Date('2022-01-01')); // ✓ +expect(Date.now(), 'to be after', new Date('2020-01-01')); // ✓ +``` + +### `{date-like} to be between {date-like} and {date-like}` + +**Aliases:** None + +**Checks:** Date is between two dates (inclusive) + +```typescript +expect( + new Date('2022-06-01'), + 'to be between', + new Date('2022-01-01'), + new Date('2022-12-31'), +); // ✓ +``` + +### `{date-like} to be the same date as {date-like}` + +**Aliases:** None + +**Checks:** Dates represent the same calendar day (ignores time) + +```typescript +expect( + new Date('2023-01-01T10:00:00'), + 'to be the same date as', + new Date('2023-01-01T15:30:00'), +); // ✓ (same date, different times) +``` + +### `{date-like} to equal {date-like} within {duration}` + +**Aliases:** None + +**Checks:** Dates are within specified duration + +**Duration formats:** "100 milliseconds", "30 seconds", "5 minutes", "2 hours", "7 days" + +```typescript +const date1 = new Date('2023-01-01T10:00:00.000Z'); +const date2 = new Date('2023-01-01T10:00:00.500Z'); +expect(date1, 'to equal', date2, 'within', '1 second'); // ✓ (500ms diff) +``` + +### `{unknown} to be a weekend` + +**Aliases:** None + +**Checks:** Date is Saturday or Sunday (in UTC) + +```typescript +expect(new Date('2023-01-07'), 'to be a weekend'); // ✓ (Saturday) +expect(new Date('2023-01-09'), 'to be a weekend'); // ✗ (Monday) +``` + +### `{unknown} to be a weekday` + +**Aliases:** None + +**Checks:** Date is Monday-Friday (in UTC) + +```typescript +expect(new Date('2023-01-09'), 'to be a weekday'); // ✓ (Monday) +expect(new Date('2023-01-07'), 'to be a weekday'); // ✗ (Saturday) +``` + +--- + +## Promise Assertions + +**Note:** All promise assertions require `expectAsync()` instead of `expect()` + +### `{Promise} to resolve` + +**Aliases:** `to fulfill` + +**Checks:** Promise resolves successfully + +```typescript +await expectAsync(Promise.resolve(42), 'to resolve'); // ✓ +await expectAsync(async () => 'result', 'to resolve'); // ✓ +await expectAsync(Promise.reject('error'), 'to resolve'); // ✗ +``` + +### `{Promise} to reject` + +**Aliases:** None + +**Checks:** Promise rejects + +```typescript +await expectAsync(Promise.reject('error'), 'to reject'); // ✓ +await expectAsync(async () => { + throw new Error('failed'); +}, 'to reject'); // ✓ +await expectAsync(Promise.resolve(42), 'to reject'); // ✗ +``` + +### `{Promise} to reject with a {constructor}` + +**Aliases:** `to reject with an` + +**Checks:** Promise rejects with specific error type + +```typescript +await expectAsync( + Promise.reject(new TypeError('Type error')), + 'to reject with a', + TypeError, +); // ✓ +``` + +### `{Promise} to reject with error satisfying {any}` + +**Aliases:** None + +**Checks:** Promise rejection matches pattern (string/RegExp/object) + +```typescript +// String matching +await expectAsync( + Promise.reject(new Error('Specific error')), + 'to reject with', + 'Specific error', +); // ✓ + +// RegExp matching +await expectAsync( + Promise.reject(new Error('Error: Something failed')), + 'to reject with', + /Something failed/, +); // ✓ + +// Object matching +await expectAsync( + Promise.reject({ message: 'Custom error', code: 500 }), + 'to reject with', + { message: 'Custom error' }, +); // ✓ +``` + +### `{Promise} to resolve with value satisfying {any}` + +**Aliases:** `to fulfill with value satisfying` + +**Checks:** Promise resolves to value matching pattern + +```typescript +// String matching +await expectAsync( + Promise.resolve('Hello World'), + 'to fulfill with value satisfying', + 'Hello World', +); // ✓ + +// RegExp matching +await expectAsync( + Promise.resolve('Success: Operation completed'), + 'to resolve to value satisfying', + /Success/, +); // ✓ + +// Object matching +await expectAsync( + Promise.resolve({ status: 'ok', data: [1, 2, 3] }), + 'to fulfill with value satisfying', + { status: 'ok' }, +); // ✓ +``` + +--- + +## Other Assertions + +### `{unknown} to be truthy` + +**Aliases:** `to exist`, `to be ok` + +**Checks:** Value is truthy + +```typescript +expect(1, 'to be truthy'); // ✓ +expect('hello', 'to be truthy'); // ✓ +expect(true, 'to exist'); // ✓ +expect(0, 'to be truthy'); // ✗ +expect('', 'to exist'); // ✗ +expect(null, 'to be ok'); // ✗ +``` + +### `{unknown} to be falsy` + +**Aliases:** None + +**Checks:** Value is falsy + +```typescript +expect(0, 'to be falsy'); // ✓ +expect('', 'to be falsy'); // ✓ +expect(false, 'to be falsy'); // ✓ +expect(null, 'to be falsy'); // ✓ +expect(undefined, 'to be falsy'); // ✓ +expect(1, 'to be falsy'); // ✗ +``` + +### `{unknown} to be defined` + +**Aliases:** None + +**Checks:** Value is not undefined + +```typescript +expect(0, 'to be defined'); // ✓ +expect('', 'to be defined'); // ✓ +expect(null, 'to be defined'); // ✓ +expect(undefined, 'to be defined'); // ✗ +``` + +--- + +## Quick Reference Tables + +### Type Checking + +| Phrase | Checks | +| ------------------ | ----------------------- | +| `to be a string` | Is a string | +| `to be a number` | Is a finite number | +| `to be a boolean` | Is a boolean | +| `to be a bigint` | Is a BigInt | +| `to be a symbol` | Is a Symbol | +| `to be an object` | Is an object (non-null) | +| `to be a function` | Is a function | +| `to be an array` | Is an Array | +| `to be a Date` | Is a Date object | +| `to be an Error` | Is an Error | +| `to be null` | Is null | +| `to be undefined` | Is undefined | +| `to be defined` | Is not undefined | + +### Comparisons + +| Phrase | Checks | +| ------------------------- | ------------------------ | +| `to equal`, `to be`, `is` | Strict equality (===) | +| `to deep equal` | Deep structural equality | +| `to be greater than` | Numeric > comparison | +| `to be less than` | Numeric < comparison | +| `to be between` | Within range (inclusive) | +| `to be one of` | Value in array | + +### Collections + +| Phrase | Checks | +| ------------------------------------ | ------------------------------ | +| `to be empty` | Length/size is 0 | +| `to have length` | Exact length/size | +| `to contain`, `to include` | Contains value | +| `to have property`, `to have key` | Has property (keypath support) | +| `to have properties`, `to have keys` | Has all specified keys | + +### Object Patterns + +| Phrase | Checks | +| ------------ | ---------------------------------------------------- | +| `to satisfy` | Partial object match with RegExp/expect.it() support | +| `to be like` | Alias for `to satisfy` | + +### Promises + +| Phrase | Requires | Checks | +| ---------------------------------- | --------------- | -------------------------- | +| `to resolve` | `expectAsync()` | Promise resolves | +| `to reject` | `expectAsync()` | Promise rejects | +| `to reject with` | `expectAsync()` | Rejection matches pattern | +| `to resolve with value satisfying` | `expectAsync()` | Resolution matches pattern | + +### Functions + +| Phrase | Checks | +| ------------------- | ----------------------------------- | +| `to throw` | Function throws | +| `to throw a {Type}` | Function throws specific error type | +| `to have arity` | Function parameter count | diff --git a/skills/bupkis-assertion-patterns/references/common_patterns.md b/skills/bupkis-assertion-patterns/references/common_patterns.md new file mode 100644 index 0000000..1f73ead --- /dev/null +++ b/skills/bupkis-assertion-patterns/references/common_patterns.md @@ -0,0 +1,603 @@ +# Common Bupkis Patterns + +Practical patterns and real-world examples for using Bupkis assertions effectively. + +## Table of Contents + +- [API Response Validation](#api-response-validation) +- [Configuration Validation](#configuration-validation) +- [Error Testing Patterns](#error-testing-patterns) +- [Async Operation Patterns](#async-operation-patterns) +- [Complex Nested Structures](#complex-nested-structures) +- [Test Data Validation](#test-data-validation) +- [Type Safety Patterns](#type-safety-patterns) + +--- + +## API Response Validation + +### REST API Success Response + +```typescript +const response = await fetch('/api/users/1'); +const data = await response.json(); + +// Validate response structure +expect(data, 'to satisfy', { + id: expect.it('to be a number'), + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + name: expect.it('to be a string', 'and', 'not to be empty'), + createdAt: expect.it('to be a valid date'), + roles: expect.it('to be an array', 'and', 'not to be empty'), +}); +``` + +### Paginated API Response + +```typescript +const response = await fetchPaginatedUsers({ page: 1, limit: 10 }); + +expect(response, 'to satisfy', { + data: expect.it('to be an array', 'and', 'to have length', 10), + meta: { + currentPage: 1, + totalPages: expect.it('to be a positive integer'), + totalItems: expect.it('to be a positive integer'), + hasNextPage: expect.it('to be a boolean'), + }, +}); + +// Validate each user in the array +expect(response.data, 'to have items satisfying', { + id: expect.it('to be a positive integer'), + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + name: expect.it('to be a string'), +}); +``` + +### Error Response Validation + +```typescript +const response = await fetchWithError('/api/invalid'); + +expect(response, 'to satisfy', { + error: { + code: expect.it('to be a string'), + message: expect.it('to be a string', 'and', 'not to be empty'), + statusCode: expect.it('to be between', 400, 599), + }, +}); +``` + +--- + +## Configuration Validation + +### Application Config + +```typescript +const config = loadAppConfig(); + +expect(config, 'to satisfy', { + port: expect.it('to be between', 1024, 65535), + host: expect.it('to be a string'), + database: { + url: /^postgresql:\/\//, + poolSize: expect.it('to be a positive integer'), + }, + logging: { + level: expect.it('to be one of', ['debug', 'info', 'warn', 'error']), + format: expect.it('to be one of', ['json', 'text']), + }, +}); +``` + +### Environment-Specific Config + +```typescript +const prodConfig = loadConfig('production'); + +expect(prodConfig, 'to satisfy', { + env: 'production', + debug: false, + apiUrl: /^https:\/\//, // Must be HTTPS in production + rateLimiting: { + enabled: true, + maxRequests: expect.it('to be a positive integer'), + }, +}); +``` + +### Feature Flags + +```typescript +const featureFlags = getFeatureFlags(); + +expect(featureFlags, 'to be an object', 'and', 'not to be empty'); + +expect(featureFlags, 'to satisfy', { + newDashboard: expect.it('to be a boolean'), + betaFeatures: expect.it('to be a boolean'), + experimentalApi: expect.it('to be a boolean'), +}); +``` + +--- + +## Error Testing Patterns + +### Standard Error Handling + +```typescript +// Function that should throw +expect(() => parseJSON('invalid json'), 'to throw a', SyntaxError); + +// With message matching +expect( + () => parseJSON('invalid json'), + 'to throw a', + SyntaxError, + 'satisfying', + { message: /Unexpected token/ }, +); +``` + +### Custom Error Properties + +```typescript +expect( + () => { + const err = new Error('Database connection failed'); + err.code = 'ECONNREFUSED'; + err.port = 5432; + throw err; + }, + 'to throw an error satisfying', + { + message: /connection failed/, + code: 'ECONNREFUSED', + port: 5432, + }, +); +``` + +### Promise Rejection + +```typescript +// Simple rejection +await expectAsync(fetchData('/api/invalid'), 'to reject'); + +// With error type +await expectAsync(fetchData('/api/invalid'), 'to reject with a', HttpError); + +// With error properties +await expectAsync( + fetchData('/api/invalid'), + 'to reject with error satisfying', + { + statusCode: 404, + message: /not found/, + }, +); +``` + +### Try-Catch Alternative + +```typescript +// ❌ DON'T use try-catch for expected errors +try { + await riskyOperation(); + expect(true, 'to be truthy'); // Maybe it works? +} catch (error) { + expect(error, 'to be an', Error); // Or maybe it throws? +} + +// ✅ DO use expectAsync with explicit contract +await expectAsync(riskyOperation(), 'to reject with a', ValidationError); +``` + +--- + +## Async Operation Patterns + +### Promise Resolution + +```typescript +// Basic resolution +await expectAsync(fetchUser(123), 'to resolve'); + +// With value validation +await expectAsync(fetchUser(123), 'to resolve with value satisfying', { + id: 123, + name: expect.it('to be a string'), + email: /^[^\s@]+@/, +}); +``` + +### Async Function Testing + +```typescript +async function processData(input) { + // ... processing + return result; +} + +await expectAsync( + async () => processData({ valid: true }), + 'to resolve with value satisfying', + { success: true }, +); + +await expectAsync( + async () => processData({ invalid: true }), + 'to reject with error satisfying', + { code: 'INVALID_INPUT' }, +); +``` + +### Race Conditions + +```typescript +// Ensure operation completes within timeout +const promise = Promise.race([slowOperation(), timeout(5000)]); + +await expectAsync(promise, 'to resolve with value satisfying', { + completed: true, +}); +``` + +--- + +## Complex Nested Structures + +### Deep Object Validation + +```typescript +const benchmark = { + name: 'Array.sort performance', + date: '2024-01-15', + results: { + samples: [1.2, 1.3, 1.1, 1.4, 1.2], + statistics: { + mean: 1.24, + median: 1.2, + stdDev: 0.11, + min: 1.1, + max: 1.4, + }, + }, + metadata: { + platform: 'Node.js v20.10.0', + arch: 'x64', + }, +}; + +expect(benchmark, 'to satisfy', { + name: expect.it('to be a string', 'and', 'not to be empty'), + date: /^\d{4}-\d{2}-\d{2}$/, + results: { + samples: expect.it('to be an array', 'and', 'to be non-empty'), + statistics: { + mean: expect.it('to be a number', 'and', 'to be positive'), + median: expect.it('to be a number', 'and', 'to be positive'), + stdDev: expect.it('to be a number'), + min: expect.it('to be a number'), + max: expect.it('to be a number'), + }, + }, + metadata: expect.it('to be an object'), +}); +``` + +### Array of Complex Objects + +```typescript +const transactions = [ + { id: 1, amount: 100.5, currency: 'USD', status: 'completed' }, + { id: 2, amount: 250.0, currency: 'EUR', status: 'pending' }, + { id: 3, amount: 75.25, currency: 'GBP', status: 'completed' }, +]; + +// Validate array structure +expect(transactions, 'to be an array', 'and', 'not to be empty'); + +// Validate each transaction +expect(transactions, 'to have items satisfying', { + id: expect.it('to be a positive integer'), + amount: expect.it('to be a positive number'), + currency: expect.it('to be one of', ['USD', 'EUR', 'GBP']), + status: expect.it('to be one of', ['pending', 'completed', 'failed']), +}); +``` + +### Optional Properties + +```typescript +const user = { + id: 123, + name: 'Alice', + email: 'alice@example.com', + // phone is optional +}; + +// Use `to satisfy` to ignore missing optional properties +expect(user, 'to satisfy', { + id: expect.it('to be a positive integer'), + name: expect.it('to be a string'), + email: /^[^\s@]+@/, + // phone not required, will pass if missing +}); + +// Explicitly check for optional property if present +if ('phone' in user) { + expect(user.phone, 'to match', /^\+?\d{10,15}$/); +} +``` + +--- + +## Test Data Validation + +### Factory-Generated Data + +```typescript +const user = UserFactory.create(); + +// Validate factory output +expect(user, 'to satisfy', { + id: expect.it('to be a positive integer'), + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + createdAt: expect.it('to be a Date'), + roles: expect.it('to be an array', 'and', 'not to be empty'), +}); +``` + +### Fixture Data + +```typescript +const fixtures = loadFixtures('users.json'); + +expect(fixtures, 'to be an array', 'and', 'to be non-empty'); + +// Validate all fixtures +expect(fixtures, 'to have items satisfying', { + id: expect.it('to be a number'), + email: expect.it('to be a string'), + name: expect.it('to be a string'), +}); +``` + +### Mock Data Consistency + +```typescript +const mockResponse = { + users: [ + /* ... */ + ], + meta: { total: 50 }, +}; + +// Ensure mock data is consistent +expect( + mockResponse.users.length, + 'to be less than or equal to', + mockResponse.meta.total, +); +``` + +--- + +## Type Safety Patterns + +### Discriminated Unions + +```typescript +type SuccessResult = { success: true; data: any }; +type ErrorResult = { success: false; error: string }; +type Result = SuccessResult | ErrorResult; + +const result: Result = processOperation(); + +if (result.success) { + expect(result, 'to satisfy', { + success: true, + data: expect.it('to be defined'), + }); +} else { + expect(result, 'to satisfy', { + success: false, + error: expect.it('to be a string', 'and', 'not to be empty'), + }); +} +``` + +### Generic Type Validation + +```typescript +function validateResponse( + response: unknown, + shape: Record, +): asserts response is T { + expect(response, 'to satisfy', shape); +} + +// Usage +const response = await fetchData(); +validateResponse(response, { + id: expect.it('to be a number'), + email: expect.it('to be a string'), +}); + +// TypeScript now knows response is User +console.log(response.email); +``` + +### Branded Types + +```typescript +type UserId = number & { __brand: 'UserId' }; + +function assertUserId(value: number): asserts value is UserId { + expect(value, 'to be a positive integer'); +} + +const id: number = 123; +assertUserId(id); +// id is now UserId +``` + +--- + +## Real-World Scenarios + +### Database Query Results + +```typescript +const users = await db.query('SELECT * FROM users WHERE active = true'); + +expect(users, 'to be an array'); +expect(users, 'to have items satisfying', { + id: expect.it('to be a positive integer'), + email: expect.it('to be a string'), + active: true, + created_at: expect.it('to be a Date'), +}); +``` + +### Form Validation + +```typescript +const formData = { + username: 'alice123', + email: 'alice@example.com', + password: 'securePassword123!', + age: 25, +}; + +expect(formData, 'to satisfy', { + username: expect.it('to be a string', 'and', 'to have length between', 3, 20), + email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, + password: expect.it('to be a string', 'and', 'to have length at least', 8), + age: expect.it('to be between', 13, 120), +}); +``` + +### File System Operations + +```typescript +const stats = await fs.stat('package.json'); + +expect(stats, 'to satisfy', { + size: expect.it('to be a positive number'), + isFile: expect.it('to be a function'), + isDirectory: expect.it('to be a function'), + mtime: expect.it('to be a Date'), +}); + +expect(stats.isFile(), 'to be true'); +``` + +### HTTP Headers Validation + +```typescript +const headers = response.headers; + +expect(headers, 'to satisfy', { + 'content-type': /^application\/json/, + 'cache-control': expect.it('to be a string'), + 'x-request-id': expect.it('to be a string', 'and', 'not to be empty'), +}); + +// Check CORS headers +expect(headers, 'to have property', 'access-control-allow-origin'); +``` + +### Event Emission Validation + +```typescript +const emitter = new EventEmitter(); +const events: any[] = []; + +emitter.on('data', (data) => events.push(data)); + +emitter.emit('data', { id: 1, value: 'test' }); +emitter.emit('data', { id: 2, value: 'test2' }); + +expect(events, 'to have length', 2); +expect(events, 'to have items satisfying', { + id: expect.it('to be a positive integer'), + value: expect.it('to be a string'), +}); +``` + +--- + +## Anti-Patterns to Avoid + +### ❌ Don't: Overly Specific Assertions + +```typescript +// Too brittle - breaks if order changes +expect(users, 'to deep equal', [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, +]); +``` + +### ✅ Do: Use Partial Matching + +```typescript +// More flexible - only checks what matters +expect(users, 'to have items satisfying', { + id: expect.it('to be a number'), + name: expect.it('to be a string'), +}); +``` + +### ❌ Don't: Redundant Checks + +```typescript +// Redundant - `to be an object` already implies non-null +expect(config, 'to be an object'); +expect(config, 'not to be null'); // Unnecessary! +``` + +### ✅ Do: Rely on Implied Checks + +```typescript +// Object check already ensures non-null +expect(config, 'to be an object'); +``` + +### ❌ Don't: Multiple Separate Assertions + +```typescript +// Fragmented - hard to see the expected structure +expect(result.exitCode, 'to equal', 0); +expect(result.stdout, 'to match', /success/); +expect(result.stderr, 'to be empty'); +``` + +### ✅ Do: Use `to satisfy` for Related Properties + +```typescript +// Clear - shows expected structure at a glance +expect(result, 'to satisfy', { + exitCode: 0, + stdout: /success/, + stderr: expect.it('to be empty'), +}); +``` + +--- + +## Tips for Writing Better Assertions + +1. **Use `to satisfy` for complex objects** - It's more maintainable than multiple assertions +2. **Leverage `expect.it()` for nested validation** - Embed assertions within object patterns +3. **Use RegExp for flexible string matching** - More robust than exact string comparisons +4. **Chain related assertions with 'and'** - Keeps related checks together +5. **Prefer semantic assertions** - Use `to have property` instead of `'key' in obj` +6. **Use `to have properties` for multiple keys** - More concise than chaining +7. **Let assertions imply related checks** - Don't redundantly check for null after object check +8. **Use `expectAsync` for promises** - Makes contract explicit: resolve or reject diff --git a/skills/skill-rules.json b/skills/skill-rules.json new file mode 100644 index 0000000..839d428 --- /dev/null +++ b/skills/skill-rules.json @@ -0,0 +1,48 @@ +{ + "description": "Skill activation rules for bupkis plugin", + "skills": { + "bupkis@boneskull-plugins:bupkis-assertion-patterns": { + "type": "domain", + "enforcement": "suggest", + "priority": "high", + "description": "Write idiomatic assertions using bupkis assertion library patterns", + "promptTriggers": { + "keywords": [ + "bupkis", + "assertion", + "expect", + "test assertion", + "bupkis pattern", + "to satisfy", + "to have property", + "write test" + ], + "intentPatterns": [ + "(write|create|add).*test", + "(write|use|add).*assertion", + "bupkis.*assertion", + "expect.*to.*satisfy", + "(how to|how do I).*bupkis" + ] + }, + "fileTriggers": { + "pathPatterns": [ + "**/*.test.ts", + "**/*.test.js", + "**/*.spec.ts", + "**/*.spec.js", + "**/test/**/*.ts", + "**/test/**/*.js" + ], + "contentPatterns": [ + "from 'bupkis'", + "from \"bupkis\"", + "require\\('bupkis'\\)", + "expect\\(.*'to satisfy'", + "expect\\(.*'to have property'" + ] + } + } + }, + "version": "1.0" +}