A lightweight, zero-configuration testing library for modern JavaScript applications with ES modules and bundler support.
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
- 🎯 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
npm install ektest- 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]);
});- Run your tests:
npx ektestThat's it! 🎉
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]);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
# Basic usage
npx ektest
# Detailed output
npx ektest --detailed
npx ektest -d
# Summary only
npx ektest --summary
npx ektest -sektest 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"
}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
ektest automatically finds test files with the pattern *.test.js in your project directory, excluding:
node_modulesdistbuildcoveragetoolsdocsexamplesscriptsvendorpublicassetsstaticbinfixturesdatatemp
// calculator.test.js
test('calculator adds numbers correctly', () => {
const result = 2 + 3;
expect('result', result).toBe(5);
});// 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']);
});// 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.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();
});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
ektest supports testing Electron applications and web applications using Puppeteer. This allows you to interact with your app's UI and test user interactions.
First, install the required dependency:
npm install --save-dev puppeteerUse 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!
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!
});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');
});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();
});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 pageSupported modifiers:
CtrlorControl- Control keyShift- Shift keyAlt- Alt keyMeta,Cmd, orCommand- 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.
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)
The object returned by query() provides the following methods and properties:
innerText(Promise): Get the inner text content of the elementinnerHTML(Promise): Get the inner HTML of the elementclick()(Promise): Click the elementcontextMenu()(Promise): Right-click the element to open context menutype(text, options?)(Promise): Type text into the element with human-like delaystext(string): The text to typeoptions.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 operationspuppeteer(Page): Access the Puppeteer page instance for complex testing scenarios
// 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);
});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,
});
});- Automatic cleanup: Cleanup is handled automatically after tests complete - no need for manual cleanup calls!
- Wait for elements: Use
page.waitForSelector()when waiting for dynamic content - Access raw Puppeteer: Use
.elementproperty to access the raw Puppeteer ElementHandle for advanced operations - Use puppeteer getter: Access
.puppeteerto get the full Puppeteer page instance for complex scenarios like screenshots, network emulation, or CDP sessions
your-project/
├── src/
│ ├── math.js
│ └── utils.js
├── tests/
│ ├── math.test.js
│ └── utils.test.js
├── package.json
└── ektest.config.json (optional)
- ✅ 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
// 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',
},
};npm install ektest
npx ektestWe welcome contributions! Please feel free to submit issues and pull requests.
MIT © Ajit Kumar
Made with ❤️ for developers who want to focus on writing tests, not configuring them.