0
# Plugin System
1
2
Plugin architecture for extending functionality with built-in and custom plugins.
3
4
## Capabilities
5
6
### Plugin Function Interface
7
8
Core plugin function signature for creating custom plugins.
9
10
```typescript { .api }
11
/**
12
* Plugin function receives an instance of the runner, emitter, config and CLI args
13
* @param japa - Plugin context with runner, emitter, config, and CLI arguments
14
*/
15
interface PluginFn {
16
(japa: {
17
config: NormalizedConfig;
18
cliArgs: CLIArgs;
19
runner: Runner;
20
emitter: Emitter;
21
}): void | Promise<void>;
22
}
23
```
24
25
### Disallow Pinned Tests Plugin
26
27
Built-in plugin that prevents pinned tests from running in production or CI environments.
28
29
```typescript { .api }
30
/**
31
* Disallows pinned tests by throwing an error before the runner starts executing tests
32
* @param options - Plugin configuration options
33
* @returns Plugin function
34
*/
35
function disallowPinnedTests(options?: {
36
/** Whether to disallow pinned tests (default: true) */
37
disallow?: boolean;
38
/** Custom error message to display */
39
errorMessage?: string;
40
}): PluginFn;
41
```
42
43
**Usage Examples:**
44
45
```typescript
46
import { configure } from "@japa/runner";
47
import { disallowPinnedTests } from "@japa/runner/plugins";
48
49
// Basic usage - disallow pinned tests
50
configure({
51
files: ["tests/**/*.spec.ts"],
52
plugins: [
53
disallowPinnedTests(),
54
],
55
});
56
57
// Custom configuration
58
configure({
59
files: ["tests/**/*.spec.ts"],
60
plugins: [
61
disallowPinnedTests({
62
disallow: process.env.NODE_ENV === "production",
63
errorMessage: "Pinned tests are not allowed in production environment",
64
}),
65
],
66
});
67
68
// Conditional based on environment
69
const isProduction = process.env.NODE_ENV === "production";
70
configure({
71
files: ["tests/**/*.spec.ts"],
72
plugins: [
73
disallowPinnedTests({
74
disallow: isProduction,
75
errorMessage: isProduction
76
? "Production builds cannot contain pinned tests"
77
: "Pinned tests found - remove .pin() before deployment",
78
}),
79
],
80
});
81
```
82
83
### Custom Plugin Creation
84
85
Create custom plugins to extend test runner functionality.
86
87
**Usage Examples:**
88
89
```typescript
90
import { configure } from "@japa/runner";
91
92
// Test timing plugin
93
const testTimingPlugin = () => {
94
const testTimes = new Map();
95
96
return ({ runner, emitter }) => {
97
emitter.on("test:start", (payload) => {
98
testTimes.set(payload.title, Date.now());
99
});
100
101
emitter.on("test:end", (payload) => {
102
const startTime = testTimes.get(payload.title);
103
if (startTime) {
104
const duration = Date.now() - startTime;
105
if (duration > 1000) {
106
console.warn(`Slow test detected: ${payload.title} (${duration}ms)`);
107
}
108
testTimes.delete(payload.title);
109
}
110
});
111
};
112
};
113
114
// Database cleanup plugin
115
const dbCleanupPlugin = () => {
116
return ({ runner, emitter, config }) => {
117
emitter.on("runner:start", async () => {
118
console.log("Setting up test database...");
119
await setupTestDatabase();
120
});
121
122
emitter.on("runner:end", async () => {
123
console.log("Cleaning up test database...");
124
await cleanupTestDatabase();
125
});
126
127
emitter.on("test:end", async (payload) => {
128
if (payload.hasError) {
129
await rollbackTestTransaction();
130
}
131
});
132
};
133
};
134
135
// Environment validation plugin
136
const envValidationPlugin = (requiredEnvVars: string[]) => {
137
return ({ config, cliArgs }) => {
138
for (const envVar of requiredEnvVars) {
139
if (!process.env[envVar]) {
140
throw new Error(`Required environment variable ${envVar} is not set`);
141
}
142
}
143
144
console.log("β All required environment variables are present");
145
};
146
};
147
148
// Coverage threshold plugin
149
const coverageThresholdPlugin = (threshold: number) => {
150
return ({ runner, emitter }) => {
151
emitter.on("runner:end", () => {
152
const summary = runner.getSummary();
153
const passRate = (summary.aggregates.passed / summary.aggregates.total) * 100;
154
155
if (passRate < threshold) {
156
console.error(`Test coverage ${passRate.toFixed(1)}% is below threshold ${threshold}%`);
157
process.exitCode = 1;
158
}
159
});
160
};
161
};
162
163
// Use custom plugins
164
configure({
165
files: ["tests/**/*.spec.ts"],
166
plugins: [
167
testTimingPlugin(),
168
dbCleanupPlugin(),
169
envValidationPlugin(["DATABASE_URL", "API_KEY"]),
170
coverageThresholdPlugin(80),
171
],
172
});
173
```
174
175
### Plugin Event System
176
177
Plugins can listen to various events throughout the test execution lifecycle.
178
179
```typescript { .api }
180
// Available events for plugin integration
181
interface PluginEvents {
182
"runner:start": () => void;
183
"runner:end": () => void;
184
"suite:start": (payload: { name: string }) => void;
185
"suite:end": (payload: { name: string; hasError: boolean }) => void;
186
"group:start": (payload: { title: string }) => void;
187
"group:end": (payload: { title: string; hasError: boolean }) => void;
188
"test:start": (payload: { title: string }) => void;
189
"test:end": (payload: {
190
title: string;
191
hasError: boolean;
192
duration: number;
193
errors: Error[];
194
}) => void;
195
}
196
```
197
198
**Usage Examples:**
199
200
```typescript
201
// Comprehensive event logging plugin
202
const eventLoggerPlugin = () => {
203
return ({ emitter }) => {
204
emitter.on("runner:start", () => {
205
console.log("π Test runner started");
206
});
207
208
emitter.on("suite:start", (payload) => {
209
console.log(`π Suite started: ${payload.name}`);
210
});
211
212
emitter.on("group:start", (payload) => {
213
console.log(`π Group started: ${payload.title}`);
214
});
215
216
emitter.on("test:start", (payload) => {
217
console.log(`π§ͺ Test started: ${payload.title}`);
218
});
219
220
emitter.on("test:end", (payload) => {
221
const status = payload.hasError ? "β" : "β ";
222
console.log(`${status} Test completed: ${payload.title} (${payload.duration}ms)`);
223
});
224
225
emitter.on("group:end", (payload) => {
226
const status = payload.hasError ? "β" : "β ";
227
console.log(`${status} Group completed: ${payload.title}`);
228
});
229
230
emitter.on("suite:end", (payload) => {
231
const status = payload.hasError ? "β" : "β ";
232
console.log(`${status} Suite completed: ${payload.name}`);
233
});
234
235
emitter.on("runner:end", () => {
236
console.log("π Test runner finished");
237
});
238
};
239
};
240
```
241
242
### Plugin Configuration Integration
243
244
Plugins can access and modify configuration during initialization.
245
246
**Usage Examples:**
247
248
```typescript
249
// Dynamic suite configuration plugin
250
const dynamicSuitePlugin = () => {
251
return ({ config, runner }) => {
252
// Add dynamic suite configuration
253
runner.onSuite((suite) => {
254
if (suite.name === "integration") {
255
suite.timeout(30000);
256
}
257
});
258
259
// Modify global configuration based on environment
260
if (process.env.NODE_ENV === "test") {
261
config.timeout = Math.max(config.timeout, 5000);
262
}
263
};
264
};
265
266
// Conditional plugin loading
267
const conditionalFeaturesPlugin = () => {
268
return ({ config, cliArgs, emitter }) => {
269
// Enable additional features based on CLI args
270
if (cliArgs.verbose) {
271
emitter.on("test:start", (payload) => {
272
console.log(`Starting test: ${payload.title}`);
273
});
274
}
275
276
// Modify configuration based on detected environment
277
if (process.env.CI) {
278
config.forceExit = true;
279
}
280
};
281
};
282
```
283
284
## Types
285
286
### Plugin Context Types
287
288
```typescript { .api }
289
interface PluginContext {
290
config: NormalizedConfig;
291
cliArgs: CLIArgs;
292
runner: Runner;
293
emitter: Emitter;
294
}
295
296
interface NormalizedConfig {
297
cwd: string;
298
timeout: number;
299
retries: number;
300
filters: Filters;
301
configureSuite: (suite: Suite) => void;
302
reporters: {
303
activated: string[];
304
list: NamedReporterContract[];
305
};
306
plugins: PluginFn[];
307
importer: (filePath: URL) => void | Promise<void>;
308
refiner: Refiner;
309
forceExit: boolean;
310
setup: SetupHookHandler[];
311
teardown: TeardownHookHandler[];
312
exclude: string[];
313
}
314
315
interface CLIArgs {
316
_?: string[];
317
tags?: string | string[];
318
files?: string | string[];
319
tests?: string | string[];
320
groups?: string | string[];
321
timeout?: string;
322
retries?: string;
323
reporters?: string | string[];
324
forceExit?: boolean;
325
failed?: boolean;
326
help?: boolean;
327
matchAll?: boolean;
328
listPinned?: boolean;
329
bail?: boolean;
330
bailLayer?: string;
331
}
332
```
333
334
### Event Payload Types
335
336
```typescript { .api }
337
interface TestEndPayload {
338
title: string;
339
hasError: boolean;
340
duration: number;
341
errors: Error[];
342
}
343
344
interface SuiteEndPayload {
345
name: string;
346
hasError: boolean;
347
}
348
349
interface GroupEndPayload {
350
title: string;
351
hasError: boolean;
352
}
353
```