@forgedevstack/crucible
v1.0.0
Published
Testing library for frontend and backend — where code is tested by fire
Downloads
84
Maintainers
Readme
🔥 Crucible
Where code is tested by fire — A zero-dependency testing library for frontend & backend
Part of the ForgeStack ecosystem.
Features
- 🧪 Full Test Framework —
describe,it,expect,beforeAll/afterAll,beforeEach/afterEach - ⚛️ React Testing — Render components, test hooks, fire events, query DOM
- 🌐 API Testing — HTTP request helpers, response assertions, mock server
- 🕵️ Spy / Mock — Function spies with call tracking, return value mocking
- ⏱️ Timeouts — Per-test and global timeouts,
waitForasync helpers - 🔗 Chainable Assertions —
.toBe(),.toEqual(),.toContain(),.toHaveStatus(),.and - 📦 Zero Dependencies — Pure TypeScript, no external packages
- 🎯 Tree-shakeable — Import only
crucible/clientorcrucible/server
Install
npm install @forgedevstack/crucible --save-devQuick Start
Unit Testing
import { describe, it, expect, spy } from '@forgedevstack/crucible';
describe('Math utils', () => {
it('adds numbers correctly', () => {
expect(1 + 2).toBe(3);
});
it('compares objects deeply', () => {
expect({ a: 1, b: { c: 2 } }).toEqual({ a: 1, b: { c: 2 } });
});
it('tracks spy calls', () => {
const fn = spy();
fn('hello');
fn('world');
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith('hello');
});
});React Component Testing
import { describe, it, expect } from '@forgedevstack/crucible';
import { render, fireEvent } from '@forgedevstack/crucible/client';
describe('Button', () => {
it('renders text', () => {
const { getByText } = render(<Button>Click me</Button>);
expect(getByText('Click me')).toBeDefined();
});
it('fires click handler', () => {
const onClick = spy();
const { getByRole } = render(<Button onClick={onClick}>Go</Button>);
fireEvent.click(getByRole('button'));
expect(onClick).toHaveBeenCalled();
});
});React Hook Testing
import { describe, it, expect } from '@forgedevstack/crucible';
import { renderHook, act } from '@forgedevstack/crucible/client';
import { useState } from 'react';
describe('useCounter', () => {
it('increments', async () => {
const { current } = renderHook(() => {
const [count, setCount] = useState(0);
return { count, increment: () => setCount(c => c + 1) };
});
expect(current.count).toBe(0);
});
});API Testing
import { describe, it, expect } from '@forgedevstack/crucible';
import { request, assertResponse, configureRoutes } from '@forgedevstack/crucible/server';
configureRoutes({
baseUrl: 'http://localhost:4000',
defaultHeaders: { 'x-api-key': 'test-key' },
});
describe('GET /api/users', () => {
it('returns users list', async () => {
const res = await request.get('/api/users');
assertResponse(res)
.toHaveStatus(200)
.toBeJson()
.and.toRespondWithin(500);
});
it('supports auth', async () => {
const res = await request.get('/api/profile', {
auth: { bearer: 'my-token' },
});
assertResponse(res)
.toHaveStatus(200)
.toHaveBodyProperty('email');
});
});Mock Server
import { describe, it, expect, beforeAll, afterAll } from '@forgedevstack/crucible';
import { createMockServer } from '@forgedevstack/crucible/server';
const server = createMockServer({
routes: [
{
method: 'GET',
path: '/api/users',
status: 200,
body: [{ id: 1, name: 'John' }],
},
{
method: 'POST',
path: '/api/users',
handler: (req) => ({
status: 201,
body: { id: 2, ...req.body },
}),
},
],
});
describe('Mock API', () => {
beforeAll(async () => {
await server.start();
});
afterAll(async () => {
await server.stop();
});
it('intercepts GET requests', async () => {
const res = await fetch('/api/users');
const data = await res.json();
expect(data).toHaveLength(1);
expect(server.getRequestCount('GET', '/api/users')).toBe(1);
});
it('intercepts POST with dynamic handler', async () => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Jane' }),
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.name).toBe('Jane');
});
});API Reference
Core
| Export | Description |
|--------|-------------|
| describe(name, fn) | Define a test suite |
| describe.skip(name, fn) | Skip a suite |
| describe.only(name, fn) | Run only this suite |
| it(name, fn, timeout?) | Define a test case |
| it.skip(name, fn) | Skip a test |
| it.only(name, fn, timeout?) | Run only this test |
| test(name, fn) | Alias for it |
| expect(value) | Create an assertion |
| beforeAll(fn) | Run before all tests in suite |
| afterAll(fn) | Run after all tests in suite |
| beforeEach(fn) | Run before each test |
| afterEach(fn) | Run after each test |
| run() | Execute all registered suites |
| configure(config) | Set global config |
Assertions (expect)
| Method | Description |
|--------|-------------|
| .toBe(expected) | Strict equality (Object.is) |
| .toEqual(expected) | Deep equality |
| .toStrictEqual(expected) | Deep + type equality |
| .toBeTruthy() | Value is truthy |
| .toBeFalsy() | Value is falsy |
| .toBeNull() | Value is null |
| .toBeUndefined() | Value is undefined |
| .toBeDefined() | Value is not undefined |
| .toBeNaN() | Value is NaN |
| .toBeGreaterThan(n) | Numeric > |
| .toBeGreaterThanOrEqual(n) | Numeric >= |
| .toBeLessThan(n) | Numeric < |
| .toBeLessThanOrEqual(n) | Numeric <= |
| .toContain(item) | Array/string contains |
| .toHaveLength(n) | Array/string length |
| .toMatch(pattern) | String match |
| .toThrow(expected?) | Function throws |
| .toHaveProperty(key, value?) | Object has property |
| .toBeInstanceOf(Class) | Instance check |
| .toHaveBeenCalled() | Spy was called |
| .toHaveBeenCalledTimes(n) | Spy call count |
| .toHaveBeenCalledWith(...args) | Spy call args |
| .toMatchObject(partial) | Partial object match |
| .toBeCloseTo(n, precision?) | Floating point |
| .not | Negate the assertion |
| .resolves | Assert on resolved promise |
| .rejects | Assert on rejected promise |
Spy / Mock
| Export | Description |
|--------|-------------|
| spy(impl?) | Create a spy function |
| spyOn(obj, method) | Spy on object method |
| .mockReturnValue(val) | Set return value |
| .mockReturnValueOnce(val) | Set one-time return |
| .mockImplementation(fn) | Replace implementation |
| .mockImplementationOnce(fn) | Replace once |
| .mockReset() | Reset spy completely |
| .mockClear() | Clear call history |
| .mockRestore() | Restore original |
Client (crucible/client)
| Export | Description |
|--------|-------------|
| render(ui, options?) | Render React component |
| renderHook(hook, options?) | Render a hook in isolation |
| cleanup() | Remove rendered components |
| fireEvent.click(el) | Simulate click |
| fireEvent.change(el, value) | Simulate input change |
| fireEvent.keyDown(el, key) | Simulate key down |
| fireEvent.submit(el) | Simulate form submit |
| fireEvent.focus(el) | Simulate focus |
| fireEvent.blur(el) | Simulate blur |
| fireEvent.scroll(el) | Simulate scroll |
| fireEvent.resize(w, h) | Simulate window resize |
| userType(el, text, delay?) | Type text character by character |
| userClear(el) | Clear an input |
| waitFor(callback, options?) | Wait for condition |
| waitForElementToBeRemoved(cb) | Wait for removal |
| act(callback) | Flush React updates |
Render Result Queries
| Query | Returns | Throws? |
|-------|---------|---------|
| getByText(text) | HTMLElement | ✅ if not found |
| getByTestId(id) | HTMLElement | ✅ if not found |
| getByRole(role) | HTMLElement | ✅ if not found |
| getByPlaceholder(text) | HTMLElement | ✅ if not found |
| getByLabel(text) | HTMLElement | ✅ if not found |
| queryByText(text) | HTMLElement \| null | ❌ |
| queryByTestId(id) | HTMLElement \| null | ❌ |
| queryByRole(role) | HTMLElement \| null | ❌ |
| getAllByText(text) | HTMLElement[] | ✅ if empty |
| getAllByRole(role) | HTMLElement[] | ✅ if empty |
| findByText(text) | Promise<HTMLElement> | ✅ on timeout |
| findByTestId(id) | Promise<HTMLElement> | ✅ on timeout |
| findByRole(role) | Promise<HTMLElement> | ✅ on timeout |
Server (crucible/server)
| Export | Description |
|--------|-------------|
| request.get(path, opts?) | Send GET request |
| request.post(path, opts?) | Send POST request |
| request.put(path, opts?) | Send PUT request |
| request.patch(path, opts?) | Send PATCH request |
| request.delete(path, opts?) | Send DELETE request |
| configureRoutes(config) | Set route test defaults |
| assertResponse(res) | Create response assertion chain |
| createMockServer(opts?) | Create a mock server |
Response Assertions
| Method | Description |
|--------|-------------|
| .toHaveStatus(code) | Check status code |
| .toHaveHeader(key, value?) | Check header exists/value |
| .toHaveBody(partial) | Partial body match |
| .toHaveBodyProperty(key, val?) | Check body property |
| .toHaveCookie(name, value?) | Check cookie |
| .toBeJson() | Content-type is JSON |
| .toRespondWithin(ms) | Response time limit |
| .toHaveContentType(type) | Check content-type |
| .and | Chain multiple assertions |
Subpath Imports
// Full library
import { describe, it, expect, render, request } from '@forgedevstack/crucible';
// Client only (React testing)
import { render, renderHook, fireEvent, cleanup } from '@forgedevstack/crucible/client';
// Server only (API testing)
import { request, assertResponse, createMockServer } from '@forgedevstack/crucible/server';Configuration
import { configure } from '@forgedevstack/crucible';
configure({
timeout: 10000, // Default test timeout (ms)
verbose: true, // Print detailed results
bail: true, // Stop on first failure
concurrency: 1, // Max parallel suites
colors: true, // ANSI colors in output
});License
MIT © John Yaghobieh
