npm package: @majkapp/plugin-test
Description: Beautiful, developer-friendly testing framework for MAJK plugins
Author: Jules White
Latest version: 1.1.0

# @majkapp/plugin-test

Beautiful, developer-friendly testing framework for MAJK plugins.

## Features

- 🎯 **Simple & Intuitive** - Clean API inspired by modern testing frameworks
- 🚀 **Fast** - Built on `uvu`, one of the fastest test runners
- 🎨 **Beautiful Output** - Rich console output with colors and formatting
- 📊 **Multiple Reporters** - Console, JSON, and HTML reports
- 🔧 **Plugin-Aware** - Built-in helpers for MAJK plugin testing
- 🧪 **Mock Support** - Easy mocking of plugin contexts and auth
- 🎭 **Function Testing** - Direct function invocation testing
- 🌐 **UI Testing** - Browser-based UI tests with Playwright and screenshot capture

## Installation

```bash
npm install --save-dev @majkapp/plugin-test
```

## Quick Start

### Initialize Test Structure

```bash
npx majk-test init
```

This creates:
```
tests/
  plugin/
    functions/
      unit/      # Mock context and data
      e2e/       # Real auth and data
    ui/
      unit/      # Mock browser
      e2e/       # Real browser and auth
```

### Write Your First Test

Create `tests/plugin/functions/unit/health.test.js`:

```javascript
import { test, assert, invoke } from '@majkapp/plugin-test';

test('health function returns ok', async () => {
  const result = await invoke('health', {});
  assert.is(result.status, 'ok');
});

test('health includes timestamp', async () => {
  const result = await invoke('health', {});
  assert.ok(result.timestamp);
});
```

### Run Tests

```bash
npx majk-test
```

## API Reference

### Core Functions

#### `test(name, fn)`

Define a test:

```javascript
test('should do something', async () => {
  // test code
});
```

#### `assert`

Assertions from `uvu/assert`:

```javascript
assert.is(actual, expected)
assert.ok(value)
assert.not(actual, expected)
assert.equal(actual, expected)  // deep equality
assert.throws(() => { ... })
```

### Mock Helpers

#### `mock(options?)`

Create a mock plugin context:

```javascript
import { mock } from '@majkapp/plugin-test';

const context = mock()
  .storage({ counter: 10 })
  .config({ apiKey: 'test-key' })
  .conversation('conv-123')
  .user('user-456')
  .build();
```

With options:

```javascript
const context = mock({
  scenario: 'empty',
  storage: { counter: 10 },
  recordCalls: true
}).build();
```

Available scenarios:
- `empty()` - Empty context
- `withBasicAuth()` - Basic auth setup
- `withStorage()` - Pre-populated storage

#### `auth`

Configure authentication:

```javascript
import { auth } from '@majkapp/plugin-test';

// From environment variables
mock().auth(auth.fromEnv('PLUGIN')).build()

// From endpoint
mock().auth(auth.fromEndpoint('http://localhost:3000/auth')).build()

// Manual account
mock().auth(auth.manual('user@example.com', 'password')).build()

// With tokens
mock().auth(auth.withTokens(
  'user@example.com',
  'password',
  'access-token',
  'refresh-token'
)).build()
```

### Invoke Helpers

#### `invoke(functionName, params, options?)`

Invoke a plugin function:

```javascript
import { invoke } from '@majkapp/plugin-test';

const result = await invoke('myFunction', { arg: 'value' });
```

With options:

```javascript
const result = await invoke('myFunction', params, {
  timeout: 5000,
  expectSuccess: true
});
```

#### `invokeAll(functionNames, params, options?)`

Invoke multiple functions:

```javascript
const results = await invokeAll(['func1', 'func2'], params);
```

## CLI Options

### Basic Usage

```bash
npx majk-test [options]
```

### Options

- `--plugin-dir, -d <dir>` - Plugin directory (default: current directory)
- `--type, -t <type>` - Test type: `functions`, `ui`, or `all` (default: `all`)
- `--env, -e <env>` - Environment: `unit`, `e2e`, or `all` (default: `all`)
- `--include, -i <pattern>` - Include pattern (can be repeated)
- `--exclude, -x <pattern>` - Exclude pattern (can be repeated)
- `--reporter, -r <reporter>` - Reporter: `console`, `json`, `html` (default: `console`)
- `--output, -o <file>` - Output file for JSON/HTML reporters
- `--bail, -b` - Stop on first failure
- `--timeout <ms>` - Test timeout in milliseconds
- `--verbose, -v` - Verbose output
- `--config, -c <file>` - Config file path

### Examples

Run all tests:
```bash
npx majk-test
```

Run only function unit tests:
```bash
npx majk-test --type functions --env unit
```

Run tests matching pattern:
```bash
npx majk-test "**/*health*"
```

Generate HTML report:
```bash
npx majk-test --reporter html --output report.html
```

Verbose output with bail:
```bash
npx majk-test --verbose --bail
```

## Configuration File

Create `majk-test.config.js`:

```javascript
module.exports = {
  type: 'all',
  env: 'all',
  reporter: 'console',
  bail: false,
  timeout: 5000,
  verbose: false,
  include: ['tests/**/*.test.js'],
  exclude: ['tests/**/*.skip.js']
};
```

Or `majk-test.config.json`:

```json
{
  "type": "functions",
  "env": "unit",
  "reporter": "console",
  "bail": true
}
```

## Test Organization

### Recommended Structure

```
tests/
  plugin/
    functions/
      unit/
        health.test.js
        storage.test.js
      e2e/
        api-integration.test.js
    ui/
      unit/
        component.test.js
      e2e/
        user-flow.test.js
```

### Naming Conventions

- Use `.test.js` or `.spec.js` suffix
- Group related tests in the same file
- Use descriptive test names

## Advanced Usage

### Test Lifecycle

```javascript
import { before, after, beforeEach, afterEach } from '@majkapp/plugin-test';

before(() => {
  // Run once before all tests
});

after(() => {
  // Run once after all tests
});

beforeEach(() => {
  // Run before each test
});

afterEach(() => {
  // Run after each test
});
```

### Skipping Tests

```javascript
test.skip('not ready yet', async () => {
  // This test will be skipped
});
```

### Only Run Specific Tests

```javascript
test.only('focus on this', async () => {
  // Only this test will run
});
```

### Call Recording

```javascript
const mockHelper = mock()
  .recordCalls()
  .build();

const callLog = mockHelper.getCallLog();
```

## UI Testing

### Using `uiTest`

The `uiTest` function provides a simple wrapper around `@majkapp/bot-plugin-server` for automated UI testing:

```javascript
const { uiTest } = require('@majkapp/plugin-test');

// Configure once for all tests
uiTest.configure({
  plugin: { dir: process.cwd() },
  browser: { headless: true, slowMo: 200 }
});

// Write UI tests
uiTest('navigate and click button', async (bot) => {
  await bot.navigate();
  await bot.screenshot('01-home');

  await bot.click('submitButton');
  await bot.screenshot('02-after-click');

  const page = bot.getPage();
  const text = await page.textContent('[data-majk-id="result"]');
  console.log(`Result: ${text}`);
});

uiTest('test with mock data', async (bot) => {
  // Configure mock handlers for this test
  bot.configure({
    handlers: {
      async getUsers() {
        return [
          { id: '1', name: 'Test User 1' },
          { id: '2', name: 'Test User 2' }
        ];
      }
    }
  });

  await bot.navigate();
  await bot.screenshot('01-users-loaded');

  // Verify mock data appeared in UI
  const page = bot.getPage();
  const userCount = await page.locator('[data-majk-id="userItem"]').count();
  console.log(`✅ Found ${userCount} users`);
});
```

### Bot API for UI Testing

The `bot` object provides these methods:

| Method | Description |
|--------|-------------|
| `navigate(url?)` | Navigate to plugin UI |
| `click(id)` | Click element with `data-majk-id` |
| `type(field, value)` | Type into `data-majk-field` |
| `waitForState(state, opts?)` | Wait for `data-majk-state` |
| `screenshot(name)` | Capture screenshot (auto-numbered) |
| `getPage()` | Get Playwright Page for advanced operations |
| `configure(config)` | Update bot configuration (e.g., handlers) |

### Screenshot Management

Screenshots are automatically organized:

```
screenshots/
  test-name/
    00-first-screenshot.png
    01-second-screenshot.png
    02-error-click-button.png  (automatic on error)
```

**Features:**
- Sequential numbering (00, 01, 02...)
- Test name as directory
- Automatic error screenshots
- Clear output paths in console

### Mock Handlers for UI Tests

Mock handlers intercept plugin API calls:

```javascript
const mockHandlers = {
  async getConversations({ limit = 100, offset = 0 }) {
    return [
      {
        id: 'conv-1',
        title: 'Plugin Architecture Discussion',
        createdAt: new Date('2025-11-10').toISOString(),
        messageCount: 15
      }
    ];
  },

  async getConversationMessages({ conversationId }) {
    return [
      {
        id: 'msg-1',
        role: 'user',
        content: 'Test message',
        timestamp: new Date().toISOString()
      }
    ];
  }
};

uiTest('verify conversations display', async (bot) => {
  bot.configure({ handlers: mockHandlers });

  await bot.navigate();
  const page = bot.getPage();

  // Navigate to conversations route
  await page.click('a:has-text("Conversations")');
  await page.waitForTimeout(1000);
  await bot.screenshot('conversations-loaded');

  // Verify mock data
  const count = await page.locator('[data-majk-id="conversationItem"]').count();
  console.log(`✅ Found ${count} conversations`);

  // Click first conversation
  await page.click('[data-majk-id="conversationItem"]:first-child');
  await page.waitForTimeout(500);
  await bot.screenshot('conversation-selected');

  // Verify messages
  const messageText = await page.textContent('[data-majk-id="messageContent"]');
  console.log(`✅ Message: ${messageText}`);
});
```

### Advanced UI Testing

Access Playwright's full API via `bot.getPage()`:

```javascript
uiTest('advanced interactions', async (bot) => {
  await bot.navigate();
  const page = bot.getPage();

  // Custom selectors
  await page.click('.my-custom-class');

  // Wait for specific elements
  await page.waitForSelector('[data-loaded="true"]', { timeout: 5000 });

  // Extract data
  const items = await page.locator('[data-majk-id="item"]').allTextContents();
  console.log(`Items: ${items.join(', ')}`);

  // Network interception
  await page.route('**/api/**', route => {
    route.fulfill({
      status: 200,
      body: JSON.stringify({ custom: 'response' })
    });
  });

  // Fill forms
  await page.fill('input[name="email"]', 'test@example.com');

  // Select dropdowns
  await page.selectOption('select[name="role"]', 'admin');

  // Standard bot API still works
  await bot.screenshot('final-state');
});
```

### Running UI Tests

```bash
# Run all UI tests
npx majk-test --type ui

# Run UI e2e tests only
npx majk-test --type ui --env e2e

# Run specific UI test
npx majk-test tests/plugin/ui/e2e/my-test.test.js

# Show browser while testing (for debugging)
# Note: use bot-plugin-server directly for this:
npx bot-plugin-server --script tests/plugin/ui/e2e/my-test.test.js --plugin . --headed --slow-mo 500
```

## Reporters

### Console Reporter (Default)

Beautiful colored output with symbols and formatting.

### JSON Reporter

Machine-readable JSON output:

```bash
npx majk-test --reporter json --output results.json
```

### HTML Reporter

Visual HTML report:

```bash
npx majk-test --reporter html --output report.html
```

## Tips & Best Practices

1. **Start with Unit Tests** - They're faster and easier to debug
2. **Use E2E for Integration** - Test real auth and data flows
3. **Keep Tests Focused** - One concept per test
4. **Use Descriptive Names** - Test names should explain what they verify
5. **Leverage Mock Scenarios** - Reuse common setups
6. **Run Tests Often** - Catch issues early

## Examples

See the `/examples` directory for complete examples:
- Basic function testing
- Auth configuration
- Mock context usage
- E2E integration tests

## License

MIT