Back to Repositories

Testing Plugin Invocation Workflow in Vue CLI

This test suite validates the plugin invocation functionality in Vue CLI, focusing on ESLint and TypeScript integration. It covers various configuration scenarios and plugin installation workflows through comprehensive unit tests.

Test Coverage Overview

The test suite provides extensive coverage of Vue CLI’s plugin invocation system, examining both inline options and interactive prompts.

  • Tests ESLint configuration with different presets (Airbnb, Standard, Prettier)
  • Validates TypeScript integration and file renaming
  • Verifies handling of existing configurations in JS and YAML formats
  • Tests git repository state validation

Implementation Analysis

The testing approach employs Jest as the primary testing framework, with mocked user prompts and filesystem operations.

  • Uses createTestProject utility for consistent test environment setup
  • Implements mock installations to simulate plugin dependencies
  • Leverages async/await patterns for asynchronous operations
  • Utilizes custom parseJS function for configuration file validation

Technical Details

  • Jest test framework with 20-second timeout
  • Inquirer for command-line prompts (mocked)
  • Custom test utilities for project creation
  • ESLint configuration parsing and validation
  • File system operations for configuration management

Best Practices Demonstrated

The test suite exemplifies robust testing practices for command-line tools and plugin systems.

  • Isolation of test cases with individual project creation
  • Comprehensive assertion chains for configuration validation
  • Proper cleanup and process management
  • Mock system for user interactions
  • Edge case handling for git repository states

vuejs/vue-cli

packages/@vue/cli/__tests__/invoke.spec.js

            
jest.setTimeout(20000)
jest.mock('inquirer')

const invoke = require('../lib/invoke')
const { expectPrompts } = require('inquirer')
const create = require('@vue/cli-test-utils/createTestProject')

const parseJS = file => {
  const res = {}
  ;(new Function('module', file))(res) // eslint-disable-line no-new-func
  return res.exports
}

const baseESLintConfig = Object.assign({}, require('@vue/cli-plugin-eslint/eslintOptions').config({
  hasPlugin: (name) => { if (name === 'babel') { return true } }
}), {
  rules: {
    'no-console': 'off',
    'no-debugger': 'off'
  }
})

async function createAndInstall (name) {
  const project = await create(name, {
    plugins: {
      '@vue/cli-plugin-babel': {}
    }
  })
  // mock install
  const pkg = JSON.parse(await project.read('package.json'))
  pkg.devDependencies['@vue/cli-plugin-eslint'] = '*'
  await project.write('package.json', JSON.stringify(pkg, null, 2))
  return project
}

async function assertUpdates (project) {
  const updatedPkg = JSON.parse(await project.read('package.json'))
  expect(updatedPkg.scripts.lint).toBe('vue-cli-service lint')
  expect(updatedPkg.devDependencies).toHaveProperty('lint-staged')
  expect(updatedPkg.gitHooks).toEqual({
    'pre-commit': 'lint-staged'
  })

  const eslintrc = parseJS(await project.read('.eslintrc.js'))
  expect(eslintrc).toEqual(Object.assign({}, baseESLintConfig, {
    extends: ['plugin:vue/essential', '@vue/airbnb']
  }))

  const lintedMain = await project.read('src/main.js')
  expect(lintedMain).toMatch(';') // should've been linted in post-generate hook
}

test('invoke with inline options', async () => {
  const project = await createAndInstall(`invoke-inline`)
  await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb --lintOn save,commit`)
  await assertUpdates(project)
})

test('invoke with prompts', async () => {
  const project = await createAndInstall(`invoke-prompts`)
  expectPrompts([
    {
      message: `Pick an ESLint config`,
      choices: [`Error prevention only`, `Airbnb`, `Standard`, `Prettier`],
      choose: 1
    },
    {
      message: `Pick additional lint features`,
      choices: [`on save`, 'on commit'],
      check: [0, 1]
    }
  ])
  // need to be in the same process to have inquirer mocked
  // so calling directly
  const cwd = process.cwd()
  // By default, @babel/eslint-parser will load babel.config.js in `path.resolve('.')`(equals `process.cwd()`) through @babel/core.
  // chdir, and let @babel/eslint-parser find the babel.config.js in our test project
  process.chdir(project.dir)

  await invoke(`eslint`, {}, project.dir)
  await assertUpdates(project)

  // restore
  process.chdir(cwd)
})

test('invoke with ts', async () => {
  const project = await create(`invoke-existing`, {
    useConfigFiles: true,
    plugins: {
      '@vue/cli-plugin-babel': {},
      '@vue/cli-plugin-eslint': { config: 'base' }
    }
  })
  // mock install
  const pkg = JSON.parse(await project.read('package.json'))
  pkg.devDependencies['@vue/cli-plugin-typescript'] = '*'
  await project.write('package.json', JSON.stringify(pkg, null, 2))

  // mock existing vue.config.js
  await project.write('vue.config.js', `module.exports = { lintOnSave: 'default' }`)

  const eslintrc = parseJS(await project.read('.eslintrc.js'))
  expect(eslintrc).toEqual(Object.assign({}, baseESLintConfig, {
    extends: ['plugin:vue/essential', 'eslint:recommended']
  }))

  await project.run(`${require.resolve('../bin/vue')} invoke typescript --classComponent --useTsWithBabel`)

  const updatedESLintrc = parseJS(await project.read('.eslintrc.js'))
  expect(updatedESLintrc).toEqual(Object.assign({}, baseESLintConfig, {
    extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/typescript'],
    parserOptions: {
      parser: '@typescript-eslint/parser'
    }
  }))
})

test('invoke with existing files (yaml)', async () => {
  const project = await create(`invoke-existing-yaml`, {
    useConfigFiles: true,
    plugins: {
      '@vue/cli-plugin-babel': {},
      '@vue/cli-plugin-eslint': {}
    }
  })

  const eslintrc = parseJS(await project.read('.eslintrc.js'))
  expect(eslintrc).toEqual(Object.assign({}, baseESLintConfig, {
    extends: ['plugin:vue/essential', 'eslint:recommended']
  }))

  await project.rm(`.eslintrc.js`)
  await project.write(`.eslintrc.yml`, `
root: true
extends:
  - plugin:vue/essential
  - eslint:recommended
  `.trim())

  await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb`)

  const updated = await project.read('.eslintrc.yml')
  expect(updated).toMatch(`
extends:
  - plugin:vue/essential
  - eslint:recommended
  - '@vue/airbnb'
`.trim())
})

test('invoking a plugin that renames files', async () => {
  const project = await create(`invoke-rename`, { plugins: {} })
  const pkg = JSON.parse(await project.read('package.json'))
  pkg.devDependencies['@vue/cli-plugin-typescript'] = '*'
  await project.write('package.json', JSON.stringify(pkg, null, 2))
  await project.run(`${require.resolve('../bin/vue')} invoke typescript -d`)
  expect(project.has('src/main.js')).toBe(false)
})

test('should prompt if invoking in a git repository with uncommitted changes', async () => {
  delete process.env.VUE_CLI_SKIP_DIRTY_GIT_PROMPT
  const project = await create('invoke-dirty', {
    plugins: {
      '@vue/cli-plugin-babel': {}
    }
  })
  await project.write('some-random-file', '')
  expectPrompts([
    {
      message: `Still proceed?`,
      confirm: true
    }
  ])
  await invoke(`babel`, {}, project.dir)
})

test.todo('invoke: should respect plugin resolveFrom')