CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-yeoman-test

Test utilities for Yeoman generators providing a fluent API for setting up test environments and asserting generated content

Overview
Eval results
Files

test-adapter.mddocs/

Test Adapter

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.

Capabilities

TestAdapter Creation

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 });

Prompt Mocking

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;
    }
  }
});

Logging Interface

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");

Prompt Interaction Testing

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;
  }
});

Integration with RunContext

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
});

Advanced Usage Patterns

Custom Spy Factory

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 mocking

Dynamic Answer Generation

const 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;
  }
});

Prompt Flow Testing

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");
  });
});

Error Handling

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

docs

generator-helpers.md

index.md

run-context.md

run-result.md

test-adapter.md

test-environment.md

tile.json