# mocha-ui-esm

[![Pipeline status](https://gitlab.com/pflannery/mocha-ui-esm/badges/master/pipeline.svg)](https://gitlab.com/pflannery/mocha-ui-esm/-/pipelines)
[![The ISC license](https://img.shields.io/badge/license-ISC-orange.png?color=blue&style=flat-square)](http://opensource.org/licenses/ISC)
[![NPM version](https://img.shields.io/npm/v/mocha-ui-esm.svg)](https://www.npmjs.org/package/mocha-ui-esm)
[![NPM downloads](https://img.shields.io/npm/dm/mocha-ui-esm.svg)](https://npmjs.org/package/mocha-ui-esm "View this project on NPM")

[![BuyMeACoffee](https://www.buymeacoffee.com/assets/img/custom_images/purple_img.png)](https://www.buymeacoffee.com/peterf)

A modern Mocha UI that allows you to define tests using ES Modules (ESM). It supports multiple test styles including Classes, Objects, and Functions, leveraging the powerful [esm-test-parser](https://gitlab.com/esm-test-group/esm-test-parser).

---

## Contents

- [Installation](#installation)
- [Usage](#usage)
  - [1. Register the UI](#1-register-the-ui)
  - [2. Configuration](#2-configuration)
    - [MochaUiEsmOptions](#mochauiesmoptions)
- [Test Styles with Examples](#test-styles-with-examples)
  - [1. Test Objects (Hierarchical & Declarative)](#1-test-objects-hierarchical--declarative)
  - [2. Test Classes (Stateful & Declarative)](#2-test-classes-stateful--declarative)
  - [3. Pure Test Functions (Functional & Simple)](#3-pure-test-functions-functional--simple)
- [License](#license)

---

## Installation

```bash
npm install mocha-ui-esm --save-dev
```

---

## Usage

### 1. Register the UI

In your test runner, register the UI before initializing Mocha.

```typescript
// runner.js
import Mocha from 'mocha';
import { registerMochaUiEsm } from 'mocha-ui-esm';
import * as MyTests from './tests.js';

// Register the 'esm' interface
registerMochaUiEsm(Mocha.interfaces);

// Create the runner
const mocha = new Mocha({
  ui: 'esm',
  reporter: 'spec'
});

// Emit your test modules to the suite
mocha.suite.emit('modules', MyTests);

mocha.run(failures => process.exit(failures ? 1 : 0));
```

### 2. Configuration

You can pass options to `registerMochaUiEsm` to customize the behavior.

```typescript
// runner.js
import Mocha from 'mocha';
import { SortByEnum, registerMochaUiEsm } from 'mocha-ui-esm';

registerMochaUiEsm(Mocha.interfaces, {
  classContextPropertyName: 'suiteContext', // Default
  sort: SortByEnum.byTitle,                 // Sort tests by title
  parserFlags: {
    allowLiteralTitle: true
  }
});
```

#### `MochaUiEsmOptions`

| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `classContextPropertyName` | `string` | `"suiteContext"` | The property name used to access the Mocha context in class instances. |
| `sort` | `SortByEnum` | `"none"` | Sorting strategy: `"none"`, `"byGroup"`, `"byTitle"`, `"byTitleAndGroup"`. |
| `testCaseTitleTemplate` | `Function` | `internal` | Custom function to generate titles for parameterized tests. |
| `parserFlags` | `Object` | `{...}` | Flags for the underlying `esm-test-parser`. |

---

## Test Styles with Examples

`mocha-ui-esm` supports three primary styles for defining your tests, each with its own advantages.

### 1. Test Objects (Hierarchical & Declarative)
Objects are ideal for hierarchical grouping where keys automatically define the structure and titles of the test suite.

*   **Hierarchical Grouping:** Organize tests into logical suites using nested structures.
*   **Implicit Titles:** Object keys serve as test and group titles.
*   **Sequential Filtering:** Use `only: true`, `only: number`, or `only: '*'` within the object to control execution.
*   **Parameterized Tests:** Use arrays of arguments to define multiple cases for a single key.

```typescript
// tests.js
import assert from 'assert';
import { test } from 'mocha-ui-esm';

export const MyMathTests = {
  [test.title]: "Math Suite",

  only: true, // Optional. runs the 'Basic Operations' suite in isolation
  'Basic Operations': {
    'Addition': {
      '1 + 1 = 2': () => { assert.strictEqual(1 + 1, 2); },
    },
  },

  // only: '*', // Optional. runs all remaining tests in the current suite in isolation

  // Parameterized test: Runs for each array entry
  'Calculate Square Root ($i)': [
    [9, 3],
    [16, 4],
    function(input, expected) {
      assert.strictEqual(Math.sqrt(input), expected);
    }
  ]
};
```

### 2. Test Classes (Stateful & Declarative)
Classes provide a declarative, stateful approach to testing, utilizing decorators for metadata.

*   **Declarative Testing:** Classes act as suites and methods act as tests.
*   **Decorators:** Use `@testCase`, `@testOnly`, and `@testTitle` for clean definitions.
*   **Instance Context:** Tests run within the class instance, allowing shared state via `this`.
*   **Context Injection:** Access the Mocha context via `this.suiteContext` (default name).

```typescript
// tests.js
import assert from 'assert';
import { testTitle, testCase, testOnly } from 'mocha-ui-esm';

@testTitle('Math Suite')
export class MathTests {
  suiteContext: any;

  @testCase(1, 1, 2)
  @testCase(2, 3, 5)
  'add $1 + $2 = $3'(a, b, expected) {
    assert.strictEqual(a + b, expected);
  }

  @testOnly()
  onlyThisTest() {
    assert.ok(true);
  }
}
```

### 3. Pure Test Functions (Functional & Simple)
Functional tests treat exported functions as individual tests, with metadata attached directly to the function objects.

*   **Automatic Discovery:** All exported functions (except reserved hooks) are treated as tests.
*   **Metadata via Properties:** Attach `[test.title]`, `[test.cases]`, and `[only]` directly to the function.
*   **Reserved Hooks:** Export functions named `beforeEach`, `afterEach`, etc., for setup/teardown.

```typescript
// tests.js
import assert from 'assert';
import { only, test } from 'mocha-ui-esm';

export function basicTest() { 
  assert.ok(true); 
}

// Attach metadata to the function object
customTest[only] = true;
customTest[test.title] = "This is a custom title";
export function customTest() { 
  assert.ok(true); 
}

// Parameterized functional test
parameterizedTest[test.cases] = [[1, 1], [2, 4]];
export function parameterizedTest(val, expected) { 
  assert.strictEqual(val * val, expected); 
}

export function beforeAll() {
  // Runs before all tests in this module
}
```

---

## License

[ISC](LICENSE)
