Skip to content

A lightweight, zero-configuration testing library for modern JavaScript applications with ES modules and bundler support.

Notifications You must be signed in to change notification settings

deadlyjack/ektest

Repository files navigation

ektest 🚀

A lightweight, zero-configuration testing library for modern JavaScript applications with ES modules and bundler support.

Why ektest?

Tired of complex configuration setups with Jest, Mocha, and other testing frameworks when working with ES modules and bundlers? ektest is designed to work out of the box with minimal to zero configuration, especially for projects using:

  • ✅ ES Modules (type: "module")
  • ✅ Webpack bundling
  • ✅ Modern JavaScript features
  • ✅ Node.js testing

Features

  • 🎯 Zero Configuration - Works out of the box
  • 📦 Bundler Integration - Built-in Webpack support
  • 🔄 ES Modules - Native support for modern JavaScript
  • 🎨 Beautiful Output - Clean, readable test results
  • 🚀 Lightweight - Minimal dependencies
  • 🔧 Configurable - Optional configuration when needed

Installation

npm install ektest

Quick Start

  1. Create a test file (e.g., math.test.js):
// math.test.js
test('addition should work', () => {
  expect('2 + 2', 2 + 2).toBe(4);
});

test('subtraction should work', () => {
  expect('5 - 3', 5 - 3).toBe(2);
});

test('arrays should contain elements', () => {
  expect('[1, 2, 3]', [1, 2, 3]).toHave([1, 2]);
});
  1. Run your tests:
npx ektest

That's it! 🎉

Available Matchers

ektest provides a comprehensive set of matchers for your assertions:

Note: The expect function takes two parameters: expect(name, value) where name is a descriptive string and value is the actual value to test.

// Equality
expect('value', value).toBe(4); // Strict equality (===)
expect('object', obj).toEqual(expected); // Deep equality for objects/arrays
expect('value', value).not.toBe(5);

// Truthiness
expect('value', value).toBeTruthy();
expect('value', value).toBeFalsy();
expect('value', value).toBeNull();
expect('value', value).toBeDefined();

// Numbers
expect('value', value).toBeGreaterThan(3);
expect('value', value).toBeLessThan(5);
expect('value', value).toBeNumber();

// Strings
expect('string', string).toMatch(/pattern/);
expect('string', string).toBeString();
expect('string', string).toContain('substring');

// Arrays and Objects
expect('array', array).toHave(item);
expect('array', array).toHave([item1, item2]); // Multiple items
expect('array', array).toContain(item); // Check if array contains item
expect('array', array).toBeArray();
expect('object', object).toHave('property');
expect('object', object).toBeObject();
expect('value', value).toBeEmpty();

// Type checking
expect('value', value).toBeInstanceOf(Array);
expect('value', value).toBeBoolean();

// Inclusion
expect('value', value).toBeIn([1, 2, 3, 4]);

Improved Error Messages

ektest provides clear, descriptive error messages with file locations and code snippets to help you quickly identify and fix issues:

test('user age validation', () => {
  const user = { name: 'John', age: 30 };
  expect('User age', user.age).toBe(25);
});

Error output:

✗ user age validation
  ✗ Comparison failed: expected values to be strictly equal (===)

    Expected: 25
    Received: 30

  at tests/user.test.js:3:30

  Code:
     1 | test('user age validation', () => {
     2 |   const user = { name: "John", age: 30 };
     3 |   expect("User age", user.age).toBe(25);
                                         ^
     4 | });

The error messages show:

  • 📝 Clear description of what failed
  • 📊 Both expected and actual values
  • 📍 Exact file location (file:line:column)
  • 🔍 Code snippet with the failing line highlighted

CLI Options

# Basic usage
npx ektest

# Detailed output
npx ektest --detailed
npx ektest -d

# Summary only
npx ektest --summary
npx ektest -s

Configuration

ektest works with zero configuration, but you can customize it by creating a ektest.config.json file:

{
  "testDir": "tests",
  "bundler": "webpack",
  "bundlerConfig": "custom-webpack.config.js"
}

Configuration Options

  • testDir (string): Directory to search for test files (default: current directory)
  • bundler (string): Bundler to use (default: "webpack")
  • bundlerConfig (string): Path to custom bundler configuration

Test File Discovery

ektest automatically finds test files with the pattern *.test.js in your project directory, excluding:

  • node_modules
  • dist
  • build
  • coverage
  • tools
  • docs
  • examples
  • scripts
  • vendor
  • public
  • assets
  • static
  • bin
  • fixtures
  • data
  • temp

Examples

Basic Test

// calculator.test.js
test('calculator adds numbers correctly', () => {
  const result = 2 + 3;
  expect('result', result).toBe(5);
});

Array and Object Tests

// collections.test.js
test('array contains elements', () => {
  const numbers = [1, 2, 3, 4, 5];
  expect('numbers', numbers).toHave(3);
  expect('numbers', numbers).toHave([1, 2]);
});

test('object has properties', () => {
  const user = { name: 'John', age: 30 };
  expect('user', user).toHave('name');
  expect('user', user).toHave(['name', 'age']);
});

Type Checking Tests

// types.test.js
test('type checking works', () => {
  expect('hello', 'hello').toBeString();
  expect('number', 42).toBeNumber();
  expect('boolean', true).toBeBoolean();
  expect('array', [1, 2, 3]).toBeArray();
  expect('object', {}).toBeObject();
});

Async Tests

// async.test.js
test('async operation works', async () => {
  const data = await fetchData();
  expect('data', data).toBeDefined();
  expect('data', data).toBeObject();
});

test('async calculation', async () => {
  const result = await new Promise((resolve) => {
    setTimeout(() => resolve(10 + 5), 100);
  });
  expect('result', result).toBe(15);
});

test('async array processing', async () => {
  const numbers = [1, 2, 3, 4, 5];
  const doubled = await Promise.all(numbers.map(async (n) => n * 2));
  expect('doubled', doubled).toHave([2, 4, 6, 8, 10]);
  expect('doubled', doubled).toBeArray();
});

Aborting Tests

You can abort test execution at any point using the abort() function. This is useful when a critical condition is met and continuing tests would be meaningless:

// abort.test.js
test('First test - this runs', async () => {
  expect('simple check', true).toBe(true);
  console.log('✓ First test passed');
});

test('Second test - abort here', async () => {
  const criticalCondition = await checkSystemState();

  if (!criticalCondition) {
    abort('Critical system error - cannot continue testing');
    return; // Exit this test
  }

  // This won't run if aborted
  expect('this check', true).toBe(true);
});

test('Third test - this will NOT run', async () => {
  // This test is skipped because abort() was called
  expect('never runs', 1).toBe(1);
});

Key points:

  • abort(message) stops all remaining tests from running
  • Aborted tests exit with code 2 (vs. 0 for pass, 1 for failures)
  • The abort message is displayed in the test summary
  • Useful for scenarios like: database connection failures, missing prerequisites, environment issues

Electron and Web App Testing with Puppeteer

ektest supports testing Electron applications and web applications using Puppeteer. This allows you to interact with your app's UI and test user interactions.

Installation

First, install the required dependency:

npm install --save-dev puppeteer

Setup

Use the setup() function (or the legacy setupElectron() alias) to launch your Electron app or web app and get a Puppeteer page instance. Cleanup is handled automatically after all tests complete - you don't need to call cleanup manually!

Testing Electron Apps

Auto-detection: If you have the electron package installed, setup() will automatically detect and use it. You only need to specify appPath if you're testing a different Electron executable.

test('Electron app launches', async () => {
  // Option 1: Auto-detect Electron (if electron package is installed)
  const { page } = await setup({
    puppeteerOptions: {
      headless: false,
      args: ['path/to/your/main.js'], // Your app's main file
    },
  });

  // Option 2: Specify custom Electron executable
  const { page } = await setup({
    appPath: 'path/to/electron.exe', // Custom Electron path
    puppeteerOptions: {
      headless: false,
      args: ['path/to/your/main.js'],
    },
  });

  // Your tests here - cleanup happens automatically!
});

Testing Web Apps

You can also test web applications by providing a url option. This launches a regular browser and navigates to the specified URL. The browser will use full viewport size for realistic testing:

test('web app login works', async () => {
  const { page } = await setup({
    url: 'http://localhost:3000', // Your web app URL
    puppeteerOptions: {
      headless: false, // Set to true for headless mode
    },
  });

  // Wait for login form to appear
  await waitFor('#username');

  // Interact with the login form
  const username = await query('#username');
  await username.type('testuser');

  const password = await query('#password');
  await password.type('password123');

  const loginButton = await query('#login-button');
  await loginButton.click();

  // Verify login was successful
  await waitFor('.dashboard', { timeout: 5000 });
  const dashboard = await query('.dashboard');
  const welcomeText = await dashboard.innerText;
  expect('welcome message', welcomeText).toContain('Welcome');
});

Query API

The query(selector) function allows you to find and interact with DOM elements in your Electron app.

The waitFor(selector, options?) function waits for an element to appear in the page before continuing.

The keyPress(keys, page?) function sends keyboard shortcuts and key combinations to the page.

test('can interact with UI elements', async () => {
  const { page } = await setup({
    url: 'http://localhost:3000',
  });

  // Wait for an element to appear
  await waitFor('#submit-button', { timeout: 5000 });

  // Query an element
  const button = await query('#submit-button');

  // Get inner text
  const text = await button.innerText;
  expect('button text', text).toBe('Submit');

  // Get inner HTML
  const html = await button.innerHTML;
  expect('button html', html).toBeString();

  // Type into an input field
  const input = await query('#username');
  await input.type('myusername'); // Types with random human-like delays

  // Type with custom delay
  const email = await query('#email');
  await email.type('[email protected]', { delay: 50 }); // 50ms between each key

  // Send keyboard shortcuts
  await keyPress('Enter'); // Press Enter key
  await keyPress('Ctrl+A'); // Select all (Ctrl+A)
  await keyPress('Shift+Enter'); // Shift+Enter combination
  await keyPress('Ctrl+Shift+K'); // Multiple modifiers
  await keyPress('Meta+V'); // Cmd+V on Mac, Win+V on Windows

  // Click the button
  await button.click();

  // Wait for a success message to appear
  await waitFor('#success-message', { timeout: 5000, visible: true });

  // Right-click for context menu
  await button.contextMenu();
});

keyPress Function

The keyPress(keys, page?) function sends keyboard shortcuts with modifiers to the page. It supports various key combinations:

// Simple keys
await keyPress('Enter');
await keyPress('Escape');
await keyPress('Tab');
await keyPress('ArrowDown');

// With modifiers (supports both + and - as separators)
await keyPress('Ctrl+A'); // Ctrl+A
await keyPress('Shift+Enter'); // Shift+Enter
await keyPress('Ctrl-Shift-K'); // Ctrl+Shift+K (dash separator)
await keyPress('Meta+C'); // Cmd+C on Mac, Win+C on Windows

// Multiple modifiers
await keyPress('Ctrl+Shift+A');
await keyPress('Alt+Shift+F');

// Using specific page (optional second parameter)
const { page } = await setup({ url: 'http://localhost:3000' });
await keyPress('Enter', page); // Explicitly pass page

Supported modifiers:

  • Ctrl or Control - Control key
  • Shift - Shift key
  • Alt - Alt key
  • Meta, Cmd, or Command - Meta/Command key (⌘ on Mac, ⊞ on Windows)

Note: The function uses the global page by default (from setup()), but you can pass a specific page instance as the second parameter if needed.

waitFor Function

The waitFor(selector, options?) function waits for an element to appear in the page:

// Wait for element to exist (default timeout: 30000ms)
await waitFor('#my-element');

// Wait with custom timeout
await waitFor('#my-element', { timeout: 5000 });

// Wait for element to be visible
await waitFor('#my-element', { visible: true });

// Wait for element to be hidden
await waitFor('#my-element', { hidden: true });

Options:

  • timeout (number, optional): Maximum time to wait in milliseconds (default: 30000)
  • visible (boolean, optional): Wait for element to be visible (default: false)
  • hidden (boolean, optional): Wait for element to be hidden (default: false)

Query Element Methods

The object returned by query() provides the following methods and properties:

  • innerText (Promise): Get the inner text content of the element
  • innerHTML (Promise): Get the inner HTML of the element
  • click() (Promise): Click the element
  • contextMenu() (Promise): Right-click the element to open context menu
  • type(text, options?) (Promise): Type text into the element with human-like delays
    • text (string): The text to type
    • options.delay (number, optional): Delay between keystrokes in milliseconds. If not specified, uses random delays between 50-150ms to mimic human typing
  • element (ElementHandle): Access the raw Puppeteer element for advanced operations
  • puppeteer (Page): Access the Puppeteer page instance for complex testing scenarios

Complete Example

// electron-app.test.js
test('Electron app full workflow', async () => {
  const { page } = await setup({
    appPath: './dist/electron/MyApp.exe',
    puppeteerOptions: {
      headless: false,
    },
  });

  // Verify app title
  const title = await query('#app-title');
  const titleText = await title.innerText;
  expect('app title', titleText).toBe('My Awesome App');

  // Fill in a form with human-like typing
  const nameInput = await query('input[name="username"]');
  await nameInput.click();
  await nameInput.type('testuser'); // Types with random delays (50-150ms)

  const emailInput = await query('input[name="email"]');
  await emailInput.click();
  await emailInput.type('[email protected]', { delay: 30 }); // Types with 30ms delay

  // Use keyboard shortcuts
  await keyPress('Tab'); // Move to next field
  await keyPress('Ctrl+A'); // Select all text
  await keyPress('Shift+Enter'); // Submit with Shift+Enter

  // Submit the form
  const submitButton = await query('button[type="submit"]');
  await submitButton.click();

  // Wait for result
  await waitFor('#success-message');

  // Verify success message
  const successMsg = await query('#success-message');
  const msgText = await successMsg.innerText;
  expect('success message', msgText).toMatch(/success/i);
});

Advanced Testing with Puppeteer

For complex testing scenarios, you can access the Puppeteer page instance directly:

test('Advanced Puppeteer operations', async () => {
  const { page } = await setup({
    appPath: './dist/electron/MyApp.exe',
  });

  // Query an element
  const button = await query('#my-button');

  // Access Puppeteer page for advanced operations
  const puppeteerPage = button.puppeteer;

  // Take a screenshot
  await puppeteerPage.screenshot({ path: 'screenshot.png' });

  // Evaluate JavaScript in the page context
  const result = await puppeteerPage.evaluate(() => {
    return window.someGlobalVariable;
  });

  // Wait for navigation
  await Promise.all([puppeteerPage.waitForNavigation(), button.click()]);

  // Emulate network conditions
  const client = await puppeteerPage.target().createCDPSession();
  await client.send('Network.emulateNetworkConditions', {
    offline: false,
    downloadThroughput: (200 * 1024) / 8,
    uploadThroughput: (200 * 1024) / 8,
    latency: 20,
  });
});

Best Practices

  1. Automatic cleanup: Cleanup is handled automatically after tests complete - no need for manual cleanup calls!
  2. Wait for elements: Use page.waitForSelector() when waiting for dynamic content
  3. Access raw Puppeteer: Use .element property to access the raw Puppeteer ElementHandle for advanced operations
  4. Use puppeteer getter: Access .puppeteer to get the full Puppeteer page instance for complex scenarios like screenshots, network emulation, or CDP sessions

Project Structure

your-project/
├── src/
│   ├── math.js
│   └── utils.js
├── tests/
│   ├── math.test.js
│   └── utils.test.js
├── package.json
└── ektest.config.json (optional)

Roadmap

  • toEqual Matcher - Deep equality comparison for objects and arrays
  • toContain Matcher - Check if arrays/strings contain elements
  • Improved Error Messages - Clear error messages with file locations and code snippets
  • Keyboard Shortcuts - keyPress function for testing keyboard interactions
  • Full Viewport Support - Web apps use full browser window size
  • 🎯 More Matchers - Add toThrow and promise matchers
  • 🌐 Browser Testing - Run tests in real browsers
  • 📊 Code Coverage - Built-in coverage reporting
  • 🔄 More Bundlers - Support for Vite, Rollup, esbuild
  • 🎯 Test Runners - Parallel test execution
  • 📸 Snapshot Testing - Visual regression testing
  • 🔍 Test Debugging - Better debugging experience

Why Choose ektest?

Before (with other frameworks)

// Complex configuration required
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  globals: {
    'ts-jest': {
      useESM: true,
    },
  },
  moduleNameMapping: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
};

After (with ektest)

npm install ektest
npx ektest

Contributing

We welcome contributions! Please feel free to submit issues and pull requests.

License

MIT © Ajit Kumar


Made with ❤️ for developers who want to focus on writing tests, not configuring them.

About

A lightweight, zero-configuration testing library for modern JavaScript applications with ES modules and bundler support.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published