A simple yet powerful testing framework for Node.js backend applications and libraries
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Plugin architecture for extending functionality with built-in and custom plugins.
Core plugin function signature for creating custom plugins.
/**
* Plugin function receives an instance of the runner, emitter, config and CLI args
* @param japa - Plugin context with runner, emitter, config, and CLI arguments
*/
interface PluginFn {
(japa: {
config: NormalizedConfig;
cliArgs: CLIArgs;
runner: Runner;
emitter: Emitter;
}): void | Promise<void>;
}Built-in plugin that prevents pinned tests from running in production or CI environments.
/**
* Disallows pinned tests by throwing an error before the runner starts executing tests
* @param options - Plugin configuration options
* @returns Plugin function
*/
function disallowPinnedTests(options?: {
/** Whether to disallow pinned tests (default: true) */
disallow?: boolean;
/** Custom error message to display */
errorMessage?: string;
}): PluginFn;Usage Examples:
import { configure } from "@japa/runner";
import { disallowPinnedTests } from "@japa/runner/plugins";
// Basic usage - disallow pinned tests
configure({
files: ["tests/**/*.spec.ts"],
plugins: [
disallowPinnedTests(),
],
});
// Custom configuration
configure({
files: ["tests/**/*.spec.ts"],
plugins: [
disallowPinnedTests({
disallow: process.env.NODE_ENV === "production",
errorMessage: "Pinned tests are not allowed in production environment",
}),
],
});
// Conditional based on environment
const isProduction = process.env.NODE_ENV === "production";
configure({
files: ["tests/**/*.spec.ts"],
plugins: [
disallowPinnedTests({
disallow: isProduction,
errorMessage: isProduction
? "Production builds cannot contain pinned tests"
: "Pinned tests found - remove .pin() before deployment",
}),
],
});Create custom plugins to extend test runner functionality.
Usage Examples:
import { configure } from "@japa/runner";
// Test timing plugin
const testTimingPlugin = () => {
const testTimes = new Map();
return ({ runner, emitter }) => {
emitter.on("test:start", (payload) => {
testTimes.set(payload.title, Date.now());
});
emitter.on("test:end", (payload) => {
const startTime = testTimes.get(payload.title);
if (startTime) {
const duration = Date.now() - startTime;
if (duration > 1000) {
console.warn(`Slow test detected: ${payload.title} (${duration}ms)`);
}
testTimes.delete(payload.title);
}
});
};
};
// Database cleanup plugin
const dbCleanupPlugin = () => {
return ({ runner, emitter, config }) => {
emitter.on("runner:start", async () => {
console.log("Setting up test database...");
await setupTestDatabase();
});
emitter.on("runner:end", async () => {
console.log("Cleaning up test database...");
await cleanupTestDatabase();
});
emitter.on("test:end", async (payload) => {
if (payload.hasError) {
await rollbackTestTransaction();
}
});
};
};
// Environment validation plugin
const envValidationPlugin = (requiredEnvVars: string[]) => {
return ({ config, cliArgs }) => {
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Required environment variable ${envVar} is not set`);
}
}
console.log("✓ All required environment variables are present");
};
};
// Coverage threshold plugin
const coverageThresholdPlugin = (threshold: number) => {
return ({ runner, emitter }) => {
emitter.on("runner:end", () => {
const summary = runner.getSummary();
const passRate = (summary.aggregates.passed / summary.aggregates.total) * 100;
if (passRate < threshold) {
console.error(`Test coverage ${passRate.toFixed(1)}% is below threshold ${threshold}%`);
process.exitCode = 1;
}
});
};
};
// Use custom plugins
configure({
files: ["tests/**/*.spec.ts"],
plugins: [
testTimingPlugin(),
dbCleanupPlugin(),
envValidationPlugin(["DATABASE_URL", "API_KEY"]),
coverageThresholdPlugin(80),
],
});Plugins can listen to various events throughout the test execution lifecycle.
// Available events for plugin integration
interface PluginEvents {
"runner:start": () => void;
"runner:end": () => void;
"suite:start": (payload: { name: string }) => void;
"suite:end": (payload: { name: string; hasError: boolean }) => void;
"group:start": (payload: { title: string }) => void;
"group:end": (payload: { title: string; hasError: boolean }) => void;
"test:start": (payload: { title: string }) => void;
"test:end": (payload: {
title: string;
hasError: boolean;
duration: number;
errors: Error[];
}) => void;
}Usage Examples:
// Comprehensive event logging plugin
const eventLoggerPlugin = () => {
return ({ emitter }) => {
emitter.on("runner:start", () => {
console.log("🏃 Test runner started");
});
emitter.on("suite:start", (payload) => {
console.log(`📂 Suite started: ${payload.name}`);
});
emitter.on("group:start", (payload) => {
console.log(`📁 Group started: ${payload.title}`);
});
emitter.on("test:start", (payload) => {
console.log(`🧪 Test started: ${payload.title}`);
});
emitter.on("test:end", (payload) => {
const status = payload.hasError ? "❌" : "✅";
console.log(`${status} Test completed: ${payload.title} (${payload.duration}ms)`);
});
emitter.on("group:end", (payload) => {
const status = payload.hasError ? "❌" : "✅";
console.log(`${status} Group completed: ${payload.title}`);
});
emitter.on("suite:end", (payload) => {
const status = payload.hasError ? "❌" : "✅";
console.log(`${status} Suite completed: ${payload.name}`);
});
emitter.on("runner:end", () => {
console.log("🏁 Test runner finished");
});
};
};Plugins can access and modify configuration during initialization.
Usage Examples:
// Dynamic suite configuration plugin
const dynamicSuitePlugin = () => {
return ({ config, runner }) => {
// Add dynamic suite configuration
runner.onSuite((suite) => {
if (suite.name === "integration") {
suite.timeout(30000);
}
});
// Modify global configuration based on environment
if (process.env.NODE_ENV === "test") {
config.timeout = Math.max(config.timeout, 5000);
}
};
};
// Conditional plugin loading
const conditionalFeaturesPlugin = () => {
return ({ config, cliArgs, emitter }) => {
// Enable additional features based on CLI args
if (cliArgs.verbose) {
emitter.on("test:start", (payload) => {
console.log(`Starting test: ${payload.title}`);
});
}
// Modify configuration based on detected environment
if (process.env.CI) {
config.forceExit = true;
}
};
};interface PluginContext {
config: NormalizedConfig;
cliArgs: CLIArgs;
runner: Runner;
emitter: Emitter;
}
interface NormalizedConfig {
cwd: string;
timeout: number;
retries: number;
filters: Filters;
configureSuite: (suite: Suite) => void;
reporters: {
activated: string[];
list: NamedReporterContract[];
};
plugins: PluginFn[];
importer: (filePath: URL) => void | Promise<void>;
refiner: Refiner;
forceExit: boolean;
setup: SetupHookHandler[];
teardown: TeardownHookHandler[];
exclude: string[];
}
interface CLIArgs {
_?: string[];
tags?: string | string[];
files?: string | string[];
tests?: string | string[];
groups?: string | string[];
timeout?: string;
retries?: string;
reporters?: string | string[];
forceExit?: boolean;
failed?: boolean;
help?: boolean;
matchAll?: boolean;
listPinned?: boolean;
bail?: boolean;
bailLayer?: string;
}interface TestEndPayload {
title: string;
hasError: boolean;
duration: number;
errors: Error[];
}
interface SuiteEndPayload {
name: string;
hasError: boolean;
}
interface GroupEndPayload {
title: string;
hasError: boolean;
}