Vimcraft Docs / vim.e2e - End-to-End Testing

vim.e2e - End-to-End Testing API

Jest-style testing framework for plugin development. Test cursor movements, mode changes, buffer content, and terminal rendering.

Running Tests

# Run tests in a directory
vimc test tests/

# Run a single test file
vimc test tests/my-plugin.ts

Basic Test Structure

describe() and test()

vim.e2e.describe('Motion Commands', function() {

  vim.e2e.test('j moves cursor down', function() {
    vim.e2e.keys('j');
    vim.e2e.assert.cursorAt(1, 0);
  });

  vim.e2e.test('w moves to next word', function() {
    vim.e2e.keys('w');
    const cursor = vim.e2e.getCursor();
    vim.e2e.assert.true(cursor.col > 0, 'Cursor should move forward');
  });

});

// Run all tests
vim.e2e.runAll();

Simulating Input

keys()

Send key sequences to the editor:

// Basic movement
vim.e2e.keys('j');      // Down
vim.e2e.keys('k');      // Up
vim.e2e.keys('gg');     // Go to top

// Enter insert mode and type
vim.e2e.keys('i');      // Enter insert mode
vim.e2e.keys('Hello');  // Type text
vim.e2e.keys('<Esc>');  // Exit insert mode

// Complex sequence
vim.e2e.keys('iHello World<Esc>0w'); // Insert, escape, go to word

Assertions

Basic Assertions

// Equality
vim.e2e.assert.equal(actual, expected, 'message');
vim.e2e.assert.deepEqual(obj1, obj2);

// Boolean
vim.e2e.assert.true(condition, 'should be true');
vim.e2e.assert.false(condition, 'should be false');

// Null checks
vim.e2e.assert.null(value);
vim.e2e.assert.notNull(value);

Editor-Specific Assertions

// Check mode
vim.e2e.assert.mode('NORMAL');
vim.e2e.assert.mode('INSERT');
vim.e2e.assert.mode('VISUAL');

// Check cursor position (0-indexed)
vim.e2e.assert.cursorAt(0, 0);  // Line 0, Column 0
vim.e2e.assert.cursorAt(5, 10); // Line 5, Column 10

// Check buffer content
vim.e2e.assert.bufferContains('Hello');
vim.e2e.assert.bufferContains('function');

Getting Editor State

getCursor()

const cursor = vim.e2e.getCursor();
console.log(`Line: ${cursor.line}, Col: ${cursor.col}`);

getMode()

const mode = vim.e2e.getMode();
console.log(`Current mode: ${mode}`);
// 'NORMAL', 'INSERT', 'VISUAL', 'VISUAL_LINE', etc.

getState()

Get complete editor state:

const state = vim.e2e.getState();
console.log(`Mode: ${state.mode}`);
console.log(`Cursor: ${state.cursor.line}, ${state.cursor.col}`);
console.log(`Lines: ${state.buffer.lineCount}`);
console.log(`Changes: ${state.buffer.changedTick}`);

PTY Testing

Test terminal rendering and escape codes:

Capture Terminal Output

vim.e2e.describe('Rendering', function() {

  vim.e2e.test('minimal cursor updates', function() {
    vim.e2e.pty.startCapture();

    vim.e2e.keys('jjjjj');
    vim.e2e.pty.render();

    vim.e2e.pty.stopCapture();

    const stats = vim.e2e.pty.getRenderStats();
    vim.e2e.assert.true(
      stats.cursorPositionCodes < 10,
      'Too many cursor position updates'
    );
  });

});

PTY Methods

// Control capture
vim.e2e.pty.startCapture();
vim.e2e.pty.stopCapture();
vim.e2e.pty.isCapturing();
vim.e2e.pty.clear();

// Trigger rendering
vim.e2e.pty.render();

// Get output
const output = vim.e2e.pty.getOutput();
const length = vim.e2e.pty.getLength();

// Count escape codes
vim.e2e.pty.countSGRCodes();         // Color codes
vim.e2e.pty.countHideCursor();       // Cursor hide
vim.e2e.pty.countShowCursor();       // Cursor show
vim.e2e.pty.countCursorPositionCodes();
vim.e2e.pty.countSequence('\x1b[2J'); // Custom sequence

// Get statistics
const stats = vim.e2e.pty.getRenderStats();

Complete Test Example

// tests/motions.ts

vim.e2e.describe('Basic Motions', function() {

  vim.e2e.test('j/k moves up and down', function() {
    vim.e2e.keys('j');
    vim.e2e.assert.cursorAt(1, 0);

    vim.e2e.keys('k');
    vim.e2e.assert.cursorAt(0, 0);
  });

  vim.e2e.test('h/l moves left and right', function() {
    vim.e2e.keys('l');
    vim.e2e.assert.cursorAt(0, 1);

    vim.e2e.keys('h');
    vim.e2e.assert.cursorAt(0, 0);
  });

});

vim.e2e.describe('Insert Mode', function() {

  vim.e2e.test('i enters insert mode', function() {
    vim.e2e.keys('i');
    vim.e2e.assert.mode('INSERT');
  });

  vim.e2e.test('Esc returns to normal', function() {
    vim.e2e.keys('i');
    vim.e2e.keys('<Esc>');
    vim.e2e.assert.mode('NORMAL');
  });

  vim.e2e.test('typing inserts text', function() {
    vim.e2e.keys('iHello<Esc>');
    vim.e2e.assert.bufferContains('Hello');
  });

});

vim.e2e.describe('Rendering Performance', function() {

  vim.e2e.test('no cursor flicker on movement', function() {
    vim.e2e.pty.startCapture();
    vim.e2e.keys('jjjjj');
    vim.e2e.pty.render();
    vim.e2e.pty.stopCapture();

    const hideCount = vim.e2e.pty.countHideCursor();
    vim.e2e.assert.true(hideCount < 3, 'Cursor should not flicker');
  });

});

// Run and report
const result = vim.e2e.runAll();
console.log(`Tests: ${result.passed} passed, ${result.failed} failed`);

Debugging Tests

checkpoint()

Add markers in logs:

vim.e2e.test('complex operation', function() {
  vim.e2e.checkpoint('Before motion');
  vim.e2e.keys('gg');

  vim.e2e.checkpoint('After goto top');
  vim.e2e.keys('G');

  vim.e2e.checkpoint('After goto bottom');
});

getLogs()

Retrieve debug logs:

const logs = vim.e2e.getLogs({
  level: 'debug',
  maxBytes: 4096
});
console.log(logs);

getLayers()

Inspect compositor layers:

const layers = vim.e2e.getLayers();
for (const layer of layers) {
  console.log(`${layer.name}: enabled=${layer.enabled}, cells=${layer.cells}`);
}

See Also