JotX is a developer-friendly testing framework that lets you write tests as React components. It combines the declarative nature of React with the expressiveness of Behavior-Driven Development (BDD) to create tests that are readable, maintainable, and powerful.
// math.test.tsx
import { AssertEqual, Call, TestCase, TestSuite, render } from "jotx";
const add = (a: number, b: number) => a + b;
render(
<TestSuite name="Math">
<TestCase name="adds numbers test case 2">
<When fn={() => add(2, 3)} as="result" />
<Then expect="result" toBe={5} />
</TestCase>
</TestSuite>
);
- π§© Declarative Testing: Write tests as React components
- π BDD-Style Syntax: Given-When-Then pattern for clear test structure
- π Async Testing: Support for asynchronous operations
- π Powerful Mocking: Mock functions, APIs, and network requests
- ποΈ Spying: Track function calls and verify interactions
- π Debugging: Enhanced debugging with context inspection
- π·οΈ Tagging: Organize tests with tags and descriptions
- π Extensible: Works with Jest, Vitest, and other test runners
npm install --save-dev jotx
# or
yarn add --dev jotx
# or
pnpm add --save-dev jotx
Create a setup file for your test runner (e.g., jest.setup.ts
):
// For Jest
import { jestRuntime } from "jotx/runtimes/jest";
import { setRuntime } from "jotx";
setRuntime(jestRuntime);
tsconfig.json
should include:
{
"compilerOptions": {
"jsx": "react-jsx",
"esModuleInterop": true
}
}
import { TestSuite, TestCase, Given, When, Then, render } from "jotx";
// Function to test
const add = (a: number, b: number) => a + b;
render(
<TestSuite name="Math Functions">
<TestCase name="adds two numbers correctly">
<Given name="a" value={2} />
<Given name="b" value={3} />
<When fn={(ctx) => add(ctx.get("a"), ctx.get("b"))} as="result" />
<Then expect="result" toBe={5} />
</TestCase>
</TestSuite>
);
npm test
# or
yarn test
# or
pnpm test
<TestCase name="fetches user data">
<MockFetch url="/api/users/1" response={{ id: 1, name: "John Doe" }} />
<WhenAsync fn={() => fetchUserData(1)} as="userData" />
<Then expect="userData.name" toEqual="John Doe" />
</TestCase>
<TestCase name="calls the logger when error occurs">
<Given name="logger" value={{ error: jest.fn() }} />
<Spy target="logger" method="error" as="loggerSpy" />
<When
fn={(ctx) => processWithErrorHandling("bad data", ctx.get("logger"))}
as="result"
/>
<Verify spy="loggerSpy" called={true} />
<Verify spy="loggerSpy" calledWith={["Error processing data: bad data"]} />
</TestCase>
<TestCase name="uses cached data when available">
<Given name="cache" value={{ get: jest.fn(), set: jest.fn() }} />
<Given name="cacheKey" value="user-123" />
<Given name="cachedData" value={{ name: "Cached User" }} />
<Mock
target="cache"
method="get"
implementation={(ctx) => (key) =>
key === ctx.get("cacheKey") ? ctx.get("cachedData") : null
}
/>
<When
fn={(ctx) => getUserWithCache(ctx.get("cacheKey"), ctx.get("cache"))}
as="result"
/>
<Then expect="result.name" toEqual="Cached User" />
</TestCase>
Groups related tests together.
<TestSuite
name="User Authentication"
description="Tests for user login and registration"
tags={["auth", "user"]}
>
{/* Test cases go here */}
</TestSuite>
Defines an individual test.
<TestCase
name="logs in successfully"
description="User should be able to log in with valid credentials"
tags={["happy-path"]}
>
{/* Test steps go here */}
</TestCase>
Sets up test preconditions.
<Given name="user" value={{ id: 1, name: "John" }} />
Executes the action being tested.
<When fn={(ctx) => login(ctx.get("credentials"))} as="result" />
Executes asynchronous actions.
<WhenAsync
fn={(ctx) => fetchUserData(ctx.get("userId"))}
as="userData"
timeout={5000} // Optional timeout in ms
/>
Asserts the expected outcome.
<Then expect="result.success" toBe={true} />
<Then expect="user.name" toEqual="John Doe" />
<Then expect="errors" toContain="Invalid email" />
<Then expect="value" toBeTrue />
<Then expect="value" toBeDefined />
<Then expect="text" toMatch={/hello/i} />
Creates a mock function.
<Mock
target="userService"
method="login"
returns={{ success: true }}
/>
<Mock
target="api"
method="fetchData"
resolves={{ data: [...] }}
/>
<Mock
target="database"
method="query"
implementation={(ctx) => [...ctx.get("mockData")]}
/>
Mocks fetch API responses.
<MockFetch
url="/api/users"
method="GET" // Optional, defaults to GET
response={[{ id: 1, name: "John" }]}
status={200} // Optional, defaults to 200
headers={{ "Content-Type": "application/json" }} // Optional
delay={500} // Optional delay in ms
/>
Creates a spy on an object method.
<Spy target="userService" method="login" as="loginSpy" />
Verifies spy/mock interactions.
<Verify spy="loginSpy" called={true} />
<Verify spy="fetchSpy" calledTimes={2} />
<Verify spy="createUserSpy" calledWith={["John", 30]} />
<Verify spy="firstSpy" calledBefore="secondSpy" />
Pauses test execution.
<Wait ms={500} description="Wait for animation to complete" />
Logs debug information.
<Debug value="userData" message="User data after login" />
<Debug logContext={true} />
JotX supports multiple test runners through runtime adapters:
- Jest:
jotx/runtimes/jest
- Vitest:
jotx/runtimes/vitest
Create your own adapter by implementing the TestRuntime
interface.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feat/amazing-feature
) - Commit your changes (
git commit -m 'feat(runtime): some amazing feature'
) - Push to the branch (
git push origin feat/amazing-feature
) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by Ink
- Built with TypeScript and React