Test utilities for Yeoman generators providing a fluent API for setting up test environments and asserting generated content
The TestAdapter provides a mock implementation of the Yeoman adapter interface, enabling precise control over user prompts and interactions during generator testing. It extends the base TestAdapter from @yeoman/adapter with additional yeoman-test specific functionality.
Create and configure TestAdapter instances for prompt mocking and interaction simulation.
/**
* TestAdapter constructor with optional configuration
* @param options - Configuration options for the adapter
*/
class TestAdapter {
constructor(options?: TestAdapterOptions);
// Inherits from @yeoman/adapter TestAdapter
prompt<T extends PromptAnswers = PromptAnswers>(
questions: PromptQuestions<T>
): Promise<T>;
log: {
write: (message?: string) => boolean;
writeln: (message?: string) => boolean;
ok: (message?: string) => boolean;
error: (message?: string) => boolean;
skip: (message?: string) => boolean;
force: (message?: string) => boolean;
create: (message?: string) => boolean;
invoke: (message?: string) => boolean;
conflict: (message?: string) => boolean;
identical: (message?: string) => boolean;
info: (message?: string) => boolean;
table: (options: { head: string[]; rows: string[][] }) => void;
};
}
interface TestAdapterOptions {
/** Pre-configured answers for prompts */
mockedAnswers?: PromptAnswers;
/** Throw error if no answer provided for a prompt */
throwOnMissingAnswer?: boolean;
/** Callback function called for each prompt answer */
callback?: DummyPromptCallback;
/** Custom spy factory for creating mocks */
spyFactory?: (options: { returns?: any }) => any;
}
type DummyPromptCallback = (
this: any,
answer: any,
options: { question: any }
) => any;
type PromptAnswers = Record<string, any>;
type PromptQuestions<T> = Array<{
type?: string;
name: keyof T;
message: string;
default?: any;
choices?: any[];
validate?: Function;
filter?: Function;
when?: Function | boolean;
}>;Usage Examples:
import { TestAdapter } from "yeoman-test";
// Basic adapter
const adapter = new TestAdapter();
// Pre-configured adapter
const adapter = new TestAdapter({
mockedAnswers: {
projectName: "my-app",
features: ["typescript", "testing"],
confirmOverwrite: true
}
});
// Use with environment
const env = await helpers.createEnv({ adapter });Configure automatic responses to generator prompts with validation and transformation.
interface TestAdapterOptions {
/** Object mapping prompt names to their answers */
mockedAnswers?: PromptAnswers;
/** Throw error when a prompt has no mocked answer (default: false) */
throwOnMissingAnswer?: boolean;
/** Custom callback to process each prompt answer */
callback?: DummyPromptCallback;
}
type DummyPromptCallback = (
this: TestAdapter,
answer: any,
options: {
question: {
name: string;
message: string;
type?: string;
default?: any;
choices?: any[];
}
}
) => any;Usage Examples:
// Simple answer mocking
const adapter = new TestAdapter({
mockedAnswers: {
projectName: "test-project",
author: "Test Author",
features: ["feature1", "feature2"],
confirmInstall: false
},
throwOnMissingAnswer: true // Fail if unexpected prompts occur
});
// Advanced prompt processing
const adapter = new TestAdapter({
mockedAnswers: {
projectName: "My Project",
email: "test@example.com"
},
callback(answer, { question }) {
console.log(`Prompt: ${question.message}`);
console.log(`Raw answer: ${answer}`);
// Transform answers based on question type
switch (question.name) {
case "projectName":
// Convert to slug format
return answer.toLowerCase().replace(/\s+/g, "-");
case "email":
// Validate email format
if (!/\S+@\S+\.\S+/.test(answer)) {
throw new Error("Invalid email format");
}
return answer.toLowerCase();
default:
return answer;
}
}
});The TestAdapter provides a comprehensive logging interface that can be mocked or monitored during tests.
interface TestAdapterLogger {
/** Write message without newline */
write(message?: string): boolean;
/** Write message with newline */
writeln(message?: string): boolean;
/** Log success message */
ok(message?: string): boolean;
/** Log error message */
error(message?: string): boolean;
/** Log skip message */
skip(message?: string): boolean;
/** Log force message */
force(message?: string): boolean;
/** Log create message */
create(message?: string): boolean;
/** Log invoke message */
invoke(message?: string): boolean;
/** Log conflict message */
conflict(message?: string): boolean;
/** Log identical message */
identical(message?: string): boolean;
/** Log info message */
info(message?: string): boolean;
/** Display table */
table(options: { head: string[]; rows: string[][] }): void;
}Usage Examples:
import { TestAdapter } from "yeoman-test";
import { mock } from "node:test";
// Mock the logger for testing
const logSpy = mock.fn();
const adapter = new TestAdapter();
// Override log methods
adapter.log.ok = logSpy;
adapter.log.error = logSpy;
// Use in generator test
await helpers.run("my-generator", {}, { adapter });
// Verify logging behavior
expect(logSpy.mock.callCount()).toBeGreaterThan(0);
expect(logSpy.mock.calls[0].arguments[0]).toContain("Success");Test complex prompt flows and conditional prompts.
// Test conditional prompts
const adapter = new TestAdapter({
mockedAnswers: {
useTypeScript: true,
tsConfigPath: "./tsconfig.json", // Only asked if useTypeScript is true
features: ["linting", "testing"],
customLinter: "eslint" // Only asked if "linting" is selected
},
callback(answer, { question }) {
// Log all prompt interactions for debugging
console.log(`Q: ${question.message}`);
console.log(`A: ${JSON.stringify(answer)}`);
return answer;
}
});
// Test validation failures
const adapter = new TestAdapter({
mockedAnswers: {
port: "3000",
email: "invalid-email" // Will trigger validation error
},
callback(answer, { question }) {
if (question.name === "email" && question.validate) {
const validationResult = question.validate(answer);
if (validationResult !== true) {
throw new Error(`Validation failed: ${validationResult}`);
}
}
return answer;
}
});TestAdapter integrates seamlessly with RunContext through adapter options.
import helpers from "yeoman-test";
// Create custom adapter
const adapter = new TestAdapter({
mockedAnswers: {
projectName: "test-app",
features: ["typescript"]
}
});
// Use with RunContext
await helpers.run("my-generator")
.withAdapterOptions({
mockedAnswers: {
additional: "answers" // Merged with adapter's answers
}
})
.withEnvironment((env) => {
// Override environment adapter
env.adapter = adapter;
});
// Alternative: Pass adapter through environment options
await helpers.run("my-generator", {}, {
adapter: adapter
});import { mock } from "node:test";
const adapter = new TestAdapter({
spyFactory: ({ returns }) => {
const spy = mock.fn();
if (returns !== undefined) {
spy.mockReturnValue(returns);
}
return spy;
}
});
// Custom spies will be used internally for prompt mockingconst adapter = new TestAdapter({
callback(answer, { question }) {
// Generate dynamic answers based on question
if (question.name === "timestamp") {
return new Date().toISOString();
}
if (question.name === "randomId") {
return Math.random().toString(36).substr(2, 9);
}
if (question.type === "list" && !answer) {
// Default to first choice for list questions
return question.choices[0];
}
return answer;
}
});describe("generator prompt flow", () => {
it("handles conditional prompts correctly", async () => {
const promptAnswers = [];
const adapter = new TestAdapter({
mockedAnswers: {
projectType: "library",
includeDocs: true,
docsFormat: "markdown" // Only asked if includeDocs is true
},
callback(answer, { question }) {
promptAnswers.push({ name: question.name, answer });
return answer;
}
});
await helpers.run("my-generator", {}, { adapter });
// Verify prompt sequence
expect(promptAnswers).toHaveLength(3);
expect(promptAnswers[0].name).toBe("projectType");
expect(promptAnswers[1].name).toBe("includeDocs");
expect(promptAnswers[2].name).toBe("docsFormat");
});
});The TestAdapter provides several error handling mechanisms:
// Strict mode - fail on unexpected prompts
const strictAdapter = new TestAdapter({
throwOnMissingAnswer: true,
mockedAnswers: {
expected: "value"
}
});
// This will throw an error if any prompts other than "expected" are encountered
// Graceful fallbacks
const flexibleAdapter = new TestAdapter({
mockedAnswers: {
primary: "answer"
},
callback(answer, { question }) {
if (answer === undefined) {
// Provide fallback for unmocked prompts
switch (question.type) {
case "confirm":
return false;
case "input":
return question.default || "";
case "list":
return question.choices?.[0];
default:
return null;
}
}
return answer;
}
});The TestAdapter provides comprehensive control over generator prompt interactions, enabling precise testing of user input scenarios and prompt flow validation.
Install with Tessl CLI
npx tessl i tessl/npm-yeoman-test