# Testing

Test your plugin with `@majkapp/plugin-test`. Write unit tests for functions and UI tests for screens.

## Setup

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

```json
// package.json
{
  "scripts": {
    "test:plugin:functions": "majk-test 'tests/plugin/functions/**/*.test.js'",
    "test:plugin:ui": "majk-test 'tests/plugin/ui/**/*.test.js'",
    "test:plugin": "majk-test 'tests/plugin/**/*.test.js'"
  }
}
```

## Function Testing

### Basic Function Test

```typescript
// tests/plugin/functions/unit/health.test.js
import { test } from '@majkapp/plugin-test';
import { invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('health check returns ok status', async () => {
  // Create mock context
  const context = mock().build();

  // Invoke function
  const result = await invoke('health', {}, { context });

  // Assert output
  assert.strictEqual(result.status, 'ok');
  assert.ok(result.timestamp);
  assert.ok(new Date(result.timestamp).getTime() > 0);
});
```

Run test:
```bash
npx majk-test tests/plugin/functions/unit/health.test.js
```

Output:
```
✓ health check returns ok status (3ms)
```

### Test with Input Parameters

```typescript
// tests/plugin/functions/unit/analytics.test.js
import { test, invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('getAnalytics returns metrics for 7d period', async () => {
  const context = mock()
    .storage({
      sessions: [
        { userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },  // 1 day ago
        { userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 }, // 2 days ago
        { userId: 'user1', duration: 600, timestamp: Date.now() - 259200000 }  // 3 days ago
      ]
    })
    .build();

  const result = await invoke('getAnalytics', { period: '7d' }, { context });

  assert.strictEqual(result.period, '7d');
  assert.strictEqual(result.metrics.activeUsers, 2);  // user1 and user2
  assert.strictEqual(result.metrics.totalSessions, 3);
  assert.strictEqual(result.metrics.avgDuration, 450);
});

test('getAnalytics filters old sessions for 24h period', async () => {
  const context = mock()
    .storage({
      sessions: [
        { userId: 'user1', duration: 300, timestamp: Date.now() - 86400000 },   // 1 day ago
        { userId: 'user2', duration: 450, timestamp: Date.now() - 172800000 },  // 2 days ago - filtered out
      ]
    })
    .build();

  const result = await invoke('getAnalytics', { period: '24h' }, { context });

  assert.strictEqual(result.metrics.activeUsers, 1);  // Only user1
  assert.strictEqual(result.metrics.totalSessions, 1);
});
```

### Test with MAJK Data

```typescript
// tests/plugin/functions/unit/dashboard.test.js
import { test, invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('getDashboardData aggregates MAJK entities', async () => {
  const context = mock()
    .withMajkData({
      conversations: [
        { id: 'conv1', title: 'Test 1', updatedAt: '2024-01-02T00:00:00Z', messages: [] },
        { id: 'conv2', title: 'Test 2', updatedAt: '2024-01-01T00:00:00Z', messages: [] }
      ],
      todos: [
        { id: 'todo1', title: 'Task 1', status: 'pending' },
        { id: 'todo2', title: 'Task 2', status: 'completed' },
        { id: 'todo3', title: 'Task 3', status: 'pending' }
      ],
      projects: [
        { id: 'proj1', name: 'Project 1' }
      ]
    })
    .build();

  const result = await invoke('getDashboardData', {}, { context });

  assert.strictEqual(result.conversations.total, 2);
  assert.strictEqual(result.conversations.recent.length, 2);
  assert.strictEqual(result.conversations.recent[0].id, 'conv1');  // Most recent first

  assert.strictEqual(result.todos.total, 3);
  assert.strictEqual(result.todos.pending, 2);
  assert.strictEqual(result.todos.completed, 1);

  assert.strictEqual(result.projects.total, 1);
  assert.ok(result.timestamp);
});
```

### Test Error Handling

```typescript
// tests/plugin/functions/unit/error-handling.test.js
import { test, invoke, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('function handles missing data gracefully', async () => {
  const context = mock()
    .storage({})  // Empty storage
    .build();

  const result = await invoke('getAnalytics', { period: '7d' }, { context });

  // Should return zero values, not crash
  assert.strictEqual(result.metrics.activeUsers, 0);
  assert.strictEqual(result.metrics.totalSessions, 0);
  assert.strictEqual(result.metrics.avgDuration, 0);
});

test('function throws error for invalid input', async () => {
  const context = mock().build();

  try {
    await invoke('getAnalytics', { period: 'invalid' }, { context });
    assert.fail('Should have thrown error');
  } catch (error) {
    assert.ok(error.message.includes('period'));
  }
});
```

## UI Testing

### Basic UI Test

```typescript
// tests/plugin/ui/unit/dashboard.test.js
import { test } from '@majkapp/plugin-test';
import { bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('dashboard displays health status', async () => {
  // Mock function responses
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .withMockHandler('getAnalytics', async () => ({
      period: '7d',
      metrics: {
        activeUsers: 42,
        totalSessions: 150,
        avgDuration: 320
      }
    }))
    .build();

  // Start browser bot
  const b = await bot(context);

  // Navigate to screen
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for data to load - check data-majk-state
  await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');

  // Verify displayed values using data-majk-id
  const healthText = await b.getText('[data-majk-id="healthStatus"]');
  assert.ok(healthText.includes('ok'), `Expected 'ok' in "${healthText}"`);

  const usersText = await b.getText('[data-majk-id="activeUsersCount"]');
  assert.ok(usersText.includes('42'), `Expected '42' in "${usersText}"`);

  // Take screenshot for visual verification
  await b.screenshot('dashboard-loaded.png');

  await b.close();
});
```

### Test Button Clicks

```typescript
// tests/plugin/ui/unit/interactions.test.js
import { test, bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('clicking refresh button updates health status', async () => {
  let healthCallCount = 0;

  const context = mock()
    .withMockHandler('health', async () => {
      healthCallCount++;
      return {
        status: 'ok',
        timestamp: new Date().toISOString()
      };
    })
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for initial load
  await b.waitForSelector('[data-majk-id="refreshHealthButton"]');
  assert.strictEqual(healthCallCount, 1, 'Initial health check should have been called');

  // Click refresh button
  await b.click('[data-majk-id="refreshHealthButton"]');

  // Wait for loading state
  await b.waitForSelector('[data-majk-id="refreshHealthButton"][data-majk-state="loading"]');

  // Wait for data to reload
  await b.waitForSelector('[data-majk-id="healthStatus"][data-majk-state="populated"]');

  // Verify second health check was called
  assert.strictEqual(healthCallCount, 2, 'Health check should have been called again');

  await b.close();
});
```

### Test State Changes

```typescript
// tests/plugin/ui/unit/toggle.test.js
import { test, bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('toggle refresh button changes state', async () => {
  const context = mock()
    .withMockHandler('health', async () => ({
      status: 'ok',
      timestamp: new Date().toISOString()
    }))
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for page load
  await b.waitForSelector('[data-majk-id="toggleRefreshButton"]');

  // Initial state should be 'refreshing'
  let state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
  assert.strictEqual(state, 'refreshing');

  // Click to pause
  await b.click('[data-majk-id="toggleRefreshButton"]');

  // Verify state changed to 'paused'
  state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
  assert.strictEqual(state, 'paused');

  // Click again to resume
  await b.click('[data-majk-id="toggleRefreshButton"]');

  // Verify state changed back to 'refreshing'
  state = await b.getAttribute('[data-majk-id="toggleRefreshButton"]', 'data-majk-state');
  assert.strictEqual(state, 'refreshing');

  await b.close();
});
```

### Test List Rendering

```typescript
// tests/plugin/ui/unit/events-list.test.js
import { test, bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('events list displays events', async () => {
  const context = mock()
    .withMockHandler('getEvents', async () => ({
      events: [
        {
          id: 'evt1',
          timestamp: '2024-01-01T00:00:00Z',
          entityType: 'conversation',
          eventType: 'created',
          entityId: 'conv1',
          entitySummary: { title: 'Test Conversation' }
        },
        {
          id: 'evt2',
          timestamp: '2024-01-02T00:00:00Z',
          entityType: 'todo',
          eventType: 'updated',
          entityId: 'todo1',
          entitySummary: { title: 'Test Todo' }
        }
      ],
      count: 2
    }))
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/events');

  // Wait for events to load
  await b.waitForSelector('[data-majk-id="eventsContainer"][data-majk-state="populated"]');

  // Verify event items are rendered
  const eventItems = await b.querySelectorAll('[data-majk-id="eventItem"]');
  assert.strictEqual(eventItems.length, 2);

  // Verify count display
  const countText = await b.getText('[data-majk-id="displayedEventsCount"]');
  assert.ok(countText.includes('2'));

  await b.close();
});

test('events list shows empty state when no events', async () => {
  const context = mock()
    .withMockHandler('getEvents', async () => ({
      events: [],
      count: 0
    }))
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/events');

  // Wait for empty state
  await b.waitForSelector('[data-majk-id="eventsContainer"][data-majk-state="empty"]');

  // Verify empty message is shown
  const emptyMessage = await b.isVisible('[data-majk-id="emptyMessage"]');
  assert.ok(emptyMessage);

  const text = await b.getText('[data-majk-id="emptyMessage"]');
  assert.ok(text.includes('No events'));

  await b.close();
});
```

### Test Error States

```typescript
// tests/plugin/ui/unit/error-handling.test.js
import { test, bot, mock } from '@majkapp/plugin-test';
import assert from 'assert';

test('dashboard displays error when health check fails', async () => {
  const context = mock()
    .withMockHandler('health', async () => {
      throw new Error('Health check failed');
    })
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');

  // Wait for error state
  await b.waitForSelector('[data-majk-id="errorMessage"][data-majk-state="error"]');

  // Verify error message is displayed
  const errorText = await b.getText('[data-majk-id="errorMessage"]');
  assert.ok(errorText.includes('Health check failed'));

  await b.screenshot('dashboard-error-state.png');

  await b.close();
});
```

## Bot API Reference

```typescript
const b = await bot(context);

// Navigation
await b.goto('/plugin-screens/my-plugin/dashboard');

// Waiting
await b.waitForSelector('[data-majk-id="element"]');
await b.waitForSelector('[data-majk-id="element"][data-majk-state="populated"]');

// Clicking
await b.click('[data-majk-id="button"]');

// Text content
const text = await b.getText('[data-majk-id="element"]');

// Attributes
const state = await b.getAttribute('[data-majk-id="element"]', 'data-majk-state');

// Visibility
const isVisible = await b.isVisible('[data-majk-id="element"]');

// Query elements
const elements = await b.querySelectorAll('[data-majk-id="item"]');

// Screenshots
await b.screenshot('test-result.png');  // Saved to tests/__screenshots__/

// Cleanup
await b.close();
```

## Mock API Reference

```typescript
// Basic mock
const context = mock().build();

// With storage
const context = mock()
  .storage({
    key1: 'value1',
    key2: { nested: 'data' }
  })
  .build();

// With MAJK data
const context = mock()
  .withMajkData({
    conversations: [{ id: 'conv1', title: 'Test' }],
    todos: [{ id: 'todo1', status: 'pending' }],
    projects: [{ id: 'proj1', name: 'Project' }],
    teammates: [{ id: 'tm1', name: 'Assistant' }],
    mcpServers: [{ id: 'mcp1', name: 'Server' }]
  })
  .build();

// With mock function handlers (for UI tests)
const context = mock()
  .withMockHandler('health', async (input) => ({
    status: 'ok',
    timestamp: new Date().toISOString()
  }))
  .withMockHandler('getAnalytics', async (input) => ({
    period: input.period,
    metrics: { activeUsers: 42, totalSessions: 150, avgDuration: 320 }
  }))
  .build();

// Combined
const context = mock()
  .storage({ sessions: [] })
  .withMajkData({ conversations: [] })
  .withMockHandler('health', async () => ({ status: 'ok' }))
  .build();
```

## Project Structure

```
my-plugin/
├── tests/
│   └── plugin/
│       ├── functions/
│       │   └── unit/
│       │       ├── health.test.js
│       │       ├── analytics.test.js
│       │       └── dashboard-data.test.js
│       └── ui/
│           └── unit/
│               ├── dashboard.test.js
│               ├── events-list.test.js
│               └── interactions.test.js
└── __screenshots__/           # Generated by UI tests
    ├── dashboard-loaded.png
    └── error-state.png
```

## Running Tests

```bash
# All tests
npm run test:plugin

# Function tests only
npm run test:plugin:functions

# UI tests only
npm run test:plugin:ui

# Single test file
npx majk-test tests/plugin/functions/unit/health.test.js

# Watch mode (re-run on file change)
npx majk-test --watch 'tests/plugin/**/*.test.js'
```

## Best Practices

### 1. Use data-majk-* Attributes

```typescript
// Good: Testable with data-majk-id
<button data-majk-id="saveButton">Save</button>

// Bad: Hard to test reliably
<button className="btn-save">Save</button>
```

### 2. Test All States

```typescript
test('component handles loading state', async () => { /* ... */ });
test('component handles empty state', async () => { /* ... */ });
test('component handles populated state', async () => { /* ... */ });
test('component handles error state', async () => { /* ... */ });
```

### 3. Verify Mock Calls

```typescript
test('function is called with correct parameters', async () => {
  let capturedInput;

  const context = mock()
    .withMockHandler('myFunction', async (input) => {
      capturedInput = input;  // Capture input for verification
      return { success: true };
    })
    .build();

  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/page');
  await b.click('[data-majk-id="submitButton"]');

  await b.waitForSelector('[data-majk-id="success"]');

  assert.deepStrictEqual(capturedInput, { expectedData: 'value' });

  await b.close();
});
```

### 4. Take Screenshots for Visual Verification

```typescript
test('dashboard renders correctly', async () => {
  const b = await bot(context);
  await b.goto('/plugin-screens/my-plugin/dashboard');
  await b.waitForSelector('[data-majk-id="dashboardContainer"][data-majk-state="populated"]');

  await b.screenshot('dashboard-populated.png');

  await b.close();
});
```

## Next Steps

Run `npx @majkapp/plugin-test` - Complete testing reference
Run `npx @majkapp/plugin-kit --config` - Set up your project for testing
