# Esm test parser

[![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/esm-test-parser.svg)](https://www.npmjs.org/package/esm-test-parser)
[![NPM downloads](https://img.shields.io/npm/dm/esm-test-parser.svg)](https://npmjs.org/package/esm-test-parser "View this project on NPM")

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

> This library is not a test runner. It's a utility for creating test runner plugins.

`esm-test-parser` is a library for converting ESM modules, classes, and objects into a structured collection of tests.
It analyzes a given JavaScript module/object and returns an array of tests which can then be added to test runners that support dynamic test creation.

## Features

- **Parameterized Tests**: Define multiple test cases.
- **Execution Filtering**: Built-in support for `only` to isolate specific tests.
- **Nested Test Context Support**: Tracks the full hierarchy of test groups, allowing you to provide a specific context (`this`) for each level of nesting via `testContextLookup`.
- **Flexible Identification**: Detects tests from functions, objects, and class instances.
- **Built-in Hooks**: Ability to specify nested custom hooks like `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`.

- **Nested Test Objects Specific**
  - **Hierarchical Grouping**: Organise tests into logical suites using nested object structures.
  - **Sequential Filtering**: Use `only: true`, `only: number`, or `only: '*'` to control execution flow within objects.
  - **Implicit Titles**: Object keys automatically serve as test and group titles.
  - **Parameterized Tests**: Use arrays of arguments to define multiple cases for a single test key.
  - **Metadata via Properties**: Use `[test.title]`, `[test.cases]`, and `[only]` directly on object and function members to override titles or define filtering and cases.

- **Pure Test Functions Specific**
  - **Metadata via Properties**: Attach `[test.title]`, `[test.cases]`, and `[only]` directly to function objects.
  - **Automatic Discovery**: All exported functions (except reserved hooks) are treated as tests.
  - **Reserved Hooks**: Export functions named `beforeEach`, `afterEach`, `beforeAll`, or `afterAll` for setup/teardown.

- **Test Classes Specific**
  - **Declarative Testing**: Use classes as suites and methods as tests.
  - **Rich Metadata Decorators**: Use `@testCase`, `@testOnly`, and `@testTitle` for clean, expressive definitions.
  - **Instance Context**: Tests run within the class instance context, allowing for shared state via `this`.
  - **Flexible Filtering**: Apply `@testOnly` to individual methods or the entire class.

## Installation

```sh
npm install esm-test-parser --save-dev
```

## Advanced Examples

### Nested Object Tests

The most flexible way to define tests is using nested objects. This allows for clear grouping and hierarchy.

```js
// Parse a module
import * as MyTests from './test-module.js' // see some examples of the test-module.js below
const tests = extractTestsFromModule(MyTests);
```

```js
// test-module.js
import { extractTestsFromModule } from 'esm-test-parser';

// 1. Define your tests using objects
export const MyMathTests = {
  'Basic Operations': {
    'Addition': {
      '1 + 1 = 2': () => { /* assert logic */ },
      '2 + 2 = 4': () => { /* assert logic */ }
    },
    'Multiplication': {
      '3 * 3 = 9': () => { /* assert logic */ }
  },
  
  // Optional: Run test(s) in isolation.
  // true: run the first preceding test, number: run [number]] preceding tests, '*': run all preceding tests.
  only: true, // can also be a number (e.g., 2) or '*'

  // Parameterized tests in objects
  'Calculate Square Root ($i)': [
    [9, 3],
    [16, 4],
    function(input, expected) {
      // assert(Math.sqrt(input) === expected)
    }
  ],

  // Optional: Built-in hooks
  beforeEach: () => { /* setup */ },
  afterEach: () => { /* teardown */ }
};
```

### Classes with Decorators

```ts
// test-module.js
import { testCase, testTitle, testOnly } from 'esm-test-parser';

@testTitle('Math Suite')
export class MathTests {

  // Optional: Test cases
  @testCase(1, 1, 2)
  @testCase(2, 3, 5)
  ['add $1 + $2 = $3'](a, b, expected) {
    // test logic
  }

  // Optional: Run test methods in isolation.
  @testOnly(true | number | '*')
  onlyThisTest() {
    // only this test will run
  }
}
```

### Functional Tests

Pure functions can be used as tests. You can attach metadata using the `test` and `only` keywords.

```js
// test-module.js
import { only, test } from 'esm-test-parser';

// A simple test function
export function basicTest() { /* ... */ }

// Optional: Run test functions in isolation. 
// true: run preceding test, number: run N preceding tests, '*': run all preceding tests.
customTest[only] = true; // can also be a number or '*'

// Optional: Set a custom title
customTest[test.title] = "This is a custom title";
export function customTest() { /* ... */ }

// Optional: Parameterized functional tests
parameterizedTest[test.title] = "Testing $1";
parameterizedTest[test.cases] = [
  ['case A'],
  ['case B']
];
export function parameterizedTest(value) {
  // Runs twice with 'case A' and 'case B'
}

// Hooks are exported as functions
export function beforeAll() { /* ... */ }
```

## API

### `extractTestsFromModule(moduleToParse, options)`

Extracts tests from a given module based on the provided options.

- `moduleToParse`: The JavaScript module or object containing test definitions.
- `options`: A `TestParserOptions` object.
  - `builtInFunctions`: Array of function names to treat as hooks.
  - `testContextLookup`: Function to resolve `this` context for tests.
  - `sort`: Sorting method (`'all'`, `'groupsOnly'`, `'testsOnly'`, `'none'`).
  - `parserFlags`: Flags to enable/disable literal property detection (e.g., `allowLiteralOnly`).

Returns a `TestCollection` (an array of `Test` objects).

## Documentation

For more detailed guides, see:
- [Testing with Classes](./docs/testing-with-classes/class-test-cases.md)
- [Testing with Objects](./docs/testing-with-objects/object-test-groups.md)
- [Testing with Pure Functions](./docs/testing-with-pure-functions/function-only.md)
- [Grouping with Module Exports](./docs/grouping-with-module-exports.md)

## License

[ISC](./LICENSE)
