Testing
Chef runs tests in a real browser via Playwright. Two types of tests are supported: unit tests (Mocha + Chai) and E2E tests (Playwright Test API).
Setup
Initialize the test environment:
chef init testsThis creates two files in the project root:
| File | Description |
|---|---|
playwright.config.ts | Playwright config for running unit and E2E tests in browser |
.env.test | Credentials for automatic authentication during tests |
Fill in your local Bitrix installation credentials:
BASE_URL=http://localhost
LOGIN=admin
PASSWORD=your_password| Variable | Description |
|---|---|
BASE_URL | URL of your local Bitrix installation |
LOGIN | Test user login |
PASSWORD | Test user password |
WARNING
Do not commit .env.test to version control — it contains sensitive credentials.
Install Playwright browsers:
npx playwright installIDE Types
mocha, chai and their types are included in Chef and used when running chef test. For IDE autocompletion, install the types locally:
npm install --save-dev @types/mocha @types/chai @playwright/testUnit Tests
Unit tests are written with Mocha + Chai and run in a real browser. The extension source code is compiled and loaded on the page alongside the tests — the actual bundle is tested as it would work in the browser.
Structure
local/js/vendor/my-extension/
└── test/
└── unit/
├── my-extension.test.ts
└── utils.test.tsBasic Test
// test/unit/my-extension.test.ts
import { describe, it, beforeEach } from 'mocha';
import { assert } from 'chai';
import { MyExtension } from '../../src/my-extension';
describe('MyExtension', () => {
let instance: MyExtension;
beforeEach(() => {
instance = new MyExtension({ name: 'test' });
});
it('should create instance with name', () => {
assert.equal(instance.getName(), 'test');
});
it('should throw on invalid name', () => {
assert.throws(() => {
new MyExtension({ name: '' });
}, TypeError);
});
});DOM Testing
Tests run in the browser, so you have full DOM access:
import { describe, it, beforeEach, afterEach } from 'mocha';
import { assert } from 'chai';
import { Button } from '../../src/button';
describe('Button', () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('should render button element', () => {
const button = new Button({ text: 'OK' });
container.appendChild(button.render());
const node = container.querySelector('.ui-btn');
assert.isNotNull(node);
assert.equal(node?.textContent, 'OK');
});
it('should handle click', () => {
let clicked = false;
const button = new Button({
text: 'OK',
onClick: () => { clicked = true; },
});
container.appendChild(button.render());
container.querySelector('.ui-btn')?.click();
assert.isTrue(clicked);
});
});Async Testing
import { describe, it } from 'mocha';
import { assert } from 'chai';
import { DataLoader } from '../../src/data-loader';
describe('DataLoader', () => {
it('should load data', async () => {
const loader = new DataLoader('/api/items');
const result = await loader.fetch();
assert.isArray(result.items);
assert.isAbove(result.items.length, 0);
});
it('should handle errors', async () => {
const loader = new DataLoader('/api/not-found');
try
{
await loader.fetch();
assert.fail('Expected error');
}
catch (error)
{
assert.instanceOf(error, Error);
}
});
});EventEmitter Testing
import { describe, it } from 'mocha';
import { assert } from 'chai';
import { Chat } from '../../src/chat';
describe('Chat', () => {
it('should emit message event', () => {
const chat = new Chat();
const messages: string[] = [];
chat.subscribe('message', (event) => {
messages.push(event.getData().text);
});
chat.sendMessage('hello');
chat.sendMessage('world');
assert.deepEqual(messages, ['hello', 'world']);
});
});E2E Tests
E2E tests use the Playwright Test API and run in a real browser on an actual Bitrix page.
Structure
local/js/vendor/my-extension/
└── test/
└── e2e/
├── my-extension.spec.ts
└── navigation.spec.tsBasic Test
// test/e2e/my-extension.spec.ts
import { test, expect } from '@playwright/test';
test('widget renders on page', async ({ page }) => {
await page.goto('/my-page/');
const widget = page.locator('.my-widget');
await expect(widget).toBeVisible();
});
test('button click shows popup', async ({ page }) => {
await page.goto('/my-page/');
await page.click('.my-widget__button');
const popup = page.locator('.popup-window');
await expect(popup).toBeVisible();
await expect(popup).toContainText('Settings');
});Authenticated Tests
For pages that require authentication, import test from ui.test.e2e.auth. Before each test, automatic login will be performed using credentials from .env.test:
import { test, expect } from 'ui.test.e2e.auth';
test('admin panel is accessible', async ({ page }) => {
// page is already authenticated
await page.goto('/bitrix/admin/');
await expect(page.locator('.adm-header')).toBeVisible();
});Working with Forms
import { test, expect } from 'ui.test.e2e.auth';
test('should save form data', async ({ page }) => {
await page.goto('/settings/');
await page.fill('input[name="title"]', 'New Title');
await page.selectOption('select[name="category"]', 'news');
await page.click('button[type="submit"]');
await expect(page.locator('.success-message')).toBeVisible();
});Waiting for AJAX Requests
import { test, expect } from 'ui.test.e2e.auth';
test('should load items via ajax', async ({ page }) => {
await page.goto('/items/');
const response = page.waitForResponse('**/ajax/**');
await page.click('.load-more');
await response;
const items = page.locator('.item-card');
await expect(items).toHaveCount(20);
});Running Tests
# All tests for an extension
chef test vendor.my-extension
# Unit tests only
chef test unit vendor.my-extension
# E2E tests only
chef test e2e vendor.my-extension
# Specific file
chef test unit vendor.my-extension ./utils.test.ts
# Tests matching pattern
chef test vendor.* --grep "should render"
# Watch mode — rerun on changes
chef test vendor.my-extension -wDebugging
# Open browser with DevTools
chef test vendor.my-extension --debug
# With visible browser window
chef test vendor.my-extension --headed
# In a specific browser
chef test vendor.my-extension --project chromiumIn --debug mode, source maps are enabled and DevTools are opened — you can set breakpoints directly in your TypeScript source code.
Tips
Test Isolation
Each test should be independent. Use beforeEach/afterEach for setup and cleanup:
describe('TodoList', () => {
let list: TodoList;
beforeEach(() => {
list = new TodoList();
});
afterEach(() => {
list.destroy();
});
it('should add item', () => {
list.add('Buy milk');
assert.equal(list.getCount(), 1);
});
it('should start empty', () => {
assert.equal(list.getCount(), 0);
});
});Test Organization
Group tests by functionality:
describe('UserService', () => {
describe('create', () => {
it('should create user with valid data', () => { /* ... */ });
it('should throw on duplicate email', () => { /* ... */ });
});
describe('update', () => {
it('should update user name', () => { /* ... */ });
it('should not allow empty name', () => { /* ... */ });
});
describe('delete', () => {
it('should soft delete user', () => { /* ... */ });
});
});