# Karma Testing

> Comprehensive Karma test runner skill for browser-based JavaScript unit testing with Jasmine, Mocha, or QUnit frameworks, real browser execution, coverage reporting, and CI/CD pipeline integration.

- Skill: `pramoddutta/karma-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pramoddutta/karma-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pramoddutta/karma-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: PramodDutta (https://skillmd.com/u/pramoddutta)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/pramoddutta/karma-testing

---


# Karma Testing

You are an expert QA engineer specializing in Karma test runner configuration and browser-based JavaScript testing. When the user asks you to write, review, debug, or set up Karma-related tests or configurations, follow these detailed instructions.

Karma is a test runner that executes JavaScript tests in real browsers. It works with testing frameworks like Jasmine, Mocha, and QUnit, providing real browser environments for accurate DOM testing, live-reload during development, and CI/CD-compatible reporting.

## Core Principles

1. **Real Browser Execution** -- Karma runs tests in actual browsers (Chrome, Firefox, Safari, Edge), catching browser-specific issues that Node.js-based runners miss. This is its primary advantage over purely Node-based test runners.
2. **Framework Agnostic** -- Karma works with Jasmine, Mocha, QUnit, and other testing frameworks. Configuration determines which framework is used. Jasmine is the most common pairing.
3. **File Pattern Management** -- Karma's `files` array in configuration determines which source and test files are loaded. Use glob patterns to include files systematically.
4. **Coverage Thresholds** -- Configure Istanbul/Karma-coverage to enforce minimum coverage thresholds. Fail the build when coverage drops below acceptable levels.
5. **Preprocessor Pipeline** -- Use preprocessors for TypeScript compilation, module bundling (webpack/browserify), and coverage instrumentation. Order matters in the pipeline.
6. **Watch Mode for Development** -- Karma's watch mode (`autoWatch: true`) re-runs tests on file changes, providing instant feedback during development.
7. **Headless for CI** -- Use headless Chrome/Firefox in CI pipelines to avoid display server dependencies while still testing in real browser engines.

## When to Use This Skill

- When configuring Karma for an Angular, React, or vanilla JavaScript project
- When setting up browser-based unit testing with Jasmine or Mocha
- When adding code coverage reporting to a Karma test suite
- When configuring Karma for CI/CD pipelines
- When debugging Karma configuration or test execution issues
- When working with `karma.conf.js`, `karma start`, or Karma plugins

## Project Structure

```
project-root/
├── karma.conf.js                   # Karma configuration
├── karma.ci.conf.js                # CI-specific overrides
├── src/
│   ├── components/
│   │   ├── calculator.js
│   │   ├── string-utils.js
│   │   └── form-validator.js
│   ├── services/
│   │   ├── api-client.js
│   │   └── storage.js
│   └── app.js
├── test/
│   ├── unit/
│   │   ├── components/
│   │   │   ├── calculator.spec.js
│   │   │   ├── string-utils.spec.js
│   │   │   └── form-validator.spec.js
│   │   └── services/
│   │       ├── api-client.spec.js
│   │       └── storage.spec.js
│   ├── helpers/
│   │   ├── test-setup.js
│   │   └── dom-helpers.js
│   └── fixtures/
│       └── mock-data.js
├── coverage/                       # Generated coverage reports
└── package.json
```

## Configuration

### karma.conf.js (Jasmine + Chrome)

```javascript
module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'src/**/*.js',
      'test/helpers/**/*.js',
      'test/unit/**/*.spec.js',
    ],
    exclude: [],
    preprocessors: {
      'src/**/*.js': ['coverage'],
    },
    reporters: ['progress', 'coverage'],
    coverageReporter: {
      type: 'html',
      dir: 'coverage/',
      subdir: '.',
      check: {
        global: {
          statements: 80,
          branches: 75,
          functions: 80,
          lines: 80,
        },
      },
    },
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    concurrency: Infinity,
    browserNoActivityTimeout: 30000,
  });
};
```

### CI Configuration (karma.ci.conf.js)

```javascript
const baseConfig = require('./karma.conf.js');

module.exports = function (config) {
  baseConfig(config);

  config.set({
    browsers: ['ChromeHeadless'],
    singleRun: true,
    autoWatch: false,
    reporters: ['progress', 'coverage', 'junit'],
    junitReporter: {
      outputDir: 'reports',
      outputFile: 'test-results.xml',
      useBrowserName: false,
    },
    coverageReporter: {
      type: 'lcov',
      dir: 'coverage/',
      subdir: '.',
      check: {
        global: {
          statements: 80,
          branches: 75,
          functions: 80,
          lines: 80,
        },
      },
    },
  });
};
```

### TypeScript + Webpack Configuration

```javascript
const webpackConfig = require('./webpack.test.config');

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      { pattern: 'test/**/*.spec.ts', watched: false },
    ],
    preprocessors: {
      'test/**/*.spec.ts': ['webpack', 'sourcemap'],
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      stats: 'errors-only',
    },
    reporters: ['progress', 'coverage-istanbul'],
    coverageIstanbulReporter: {
      reports: ['html', 'lcovonly', 'text-summary'],
      dir: 'coverage/',
      fixWebpackSourcePaths: true,
      thresholds: {
        emitWarning: false,
        global: {
          statements: 80,
          lines: 80,
          branches: 75,
          functions: 80,
        },
      },
    },
    browsers: ['ChromeHeadless'],
    singleRun: true,
  });
};
```

## Writing Tests (Jasmine)

### Basic Unit Tests

```javascript
describe('Calculator', () => {
  let calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  describe('add', () => {
    it('should add two positive numbers', () => {
      expect(calculator.add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-1, -2)).toBe(-3);
    });

    it('should handle zero', () => {
      expect(calculator.add(0, 5)).toBe(5);
      expect(calculator.add(5, 0)).toBe(5);
    });

    it('should handle floating point numbers', () => {
      expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3, 10);
    });
  });

  describe('divide', () => {
    it('should divide two numbers', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw on division by zero', () => {
      expect(() => calculator.divide(10, 0)).toThrowError('Division by zero');
    });
  });
});
```

### String Utility Tests

```javascript
describe('StringUtils', () => {
  describe('capitalize', () => {
    it('should capitalize the first letter', () => {
      expect(StringUtils.capitalize('hello')).toBe('Hello');
    });

    it('should handle empty strings', () => {
      expect(StringUtils.capitalize('')).toBe('');
    });

    it('should handle already capitalized strings', () => {
      expect(StringUtils.capitalize('Hello')).toBe('Hello');
    });

    it('should handle single characters', () => {
      expect(StringUtils.capitalize('a')).toBe('A');
    });
  });

  describe('truncate', () => {
    it('should truncate long strings with ellipsis', () => {
      const result = StringUtils.truncate('This is a very long string', 10);
      expect(result).toBe('This is a ...');
      expect(result.length).toBeLessThanOrEqual(13);
    });

    it('should not truncate short strings', () => {
      expect(StringUtils.truncate('Short', 10)).toBe('Short');
    });
  });
});
```

### DOM Testing

```javascript
describe('Form Validator', () => {
  let container;
  let validator;

  beforeEach(() => {
    container = document.createElement('div');
    container.innerHTML = `
      <form id="test-form">
        <input type="text" id="name" data-testid="name-input" />
        <input type="email" id="email" data-testid="email-input" />
        <input type="password" id="password" data-testid="password-input" />
        <span id="name-error" class="error" data-testid="name-error"></span>
        <span id="email-error" class="error" data-testid="email-error"></span>
        <button type="submit" data-testid="submit-btn">Submit</button>
      </form>
    `;
    document.body.appendChild(container);
    validator = new FormValidator('#test-form');
  });

  afterEach(() => {
    document.body.removeChild(container);
  });

  it('should validate required name field', () => {
    const nameInput = document.querySelector('[data-testid="name-input"]');
    nameInput.value = '';
    const result = validator.validateField('name');
    expect(result.valid).toBe(false);
    expect(result.message).toBe('Name is required');
  });

  it('should validate email format', () => {
    const emailInput = document.querySelector('[data-testid="email-input"]');
    emailInput.value = 'not-an-email';
    const result = validator.validateField('email');
    expect(result.valid).toBe(false);
    expect(result.message).toContain('valid email');
  });

  it('should show error messages in DOM', () => {
    const nameInput = document.querySelector('[data-testid="name-input"]');
    nameInput.value = '';
    validator.validate();
    const errorEl = document.querySelector('[data-testid="name-error"]');
    expect(errorEl.textContent).toBe('Name is required');
    expect(errorEl.classList.contains('visible')).toBe(true);
  });

  it('should enable submit on valid form', () => {
    document.querySelector('[data-testid="name-input"]').value = 'John';
    document.querySelector('[data-testid="email-input"]').value = 'john@example.com';
    document.querySelector('[data-testid="password-input"]').value = 'SecurePass123!';

    validator.validate();
    const submitBtn = document.querySelector('[data-testid="submit-btn"]');
    expect(submitBtn.disabled).toBe(false);
  });
});
```

### Async Testing

```javascript
describe('ApiClient', () => {
  let apiClient;

  beforeEach(() => {
    apiClient = new ApiClient('http://localhost:3000/api');
  });

  it('should fetch users successfully', async () => {
    spyOn(window, 'fetch').and.returnValue(
      Promise.resolve(
        new Response(JSON.stringify([{ id: 1, name: 'Alice' }]), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        })
      )
    );

    const users = await apiClient.getUsers();
    expect(users).toEqual([{ id: 1, name: 'Alice' }]);
    expect(window.fetch).toHaveBeenCalledWith('http://localhost:3000/api/users', jasmine.any(Object));
  });

  it('should handle API errors', async () => {
    spyOn(window, 'fetch').and.returnValue(
      Promise.resolve(new Response('Not Found', { status: 404 }))
    );

    try {
      await apiClient.getUsers();
      fail('Expected an error to be thrown');
    } catch (error) {
      expect(error.message).toContain('404');
    }
  });

  it('should handle network failures', async () => {
    spyOn(window, 'fetch').and.returnValue(Promise.reject(new Error('Network error')));

    try {
      await apiClient.getUsers();
      fail('Expected an error to be thrown');
    } catch (error) {
      expect(error.message).toBe('Network error');
    }
  });
});
```

### Jasmine Spies and Mocks

```javascript
describe('EventTracker', () => {
  let tracker;
  let mockStorage;

  beforeEach(() => {
    mockStorage = jasmine.createSpyObj('Storage', ['getItem', 'setItem', 'removeItem']);
    tracker = new EventTracker(mockStorage);
  });

  it('should store events in storage', () => {
    tracker.track('page_view', { page: '/home' });

    expect(mockStorage.setItem).toHaveBeenCalledWith(
      jasmine.stringMatching(/^event_/),
      jasmine.stringContaining('"type":"page_view"')
    );
  });

  it('should retrieve event count', () => {
    mockStorage.getItem.and.returnValue(JSON.stringify({ count: 5 }));

    const count = tracker.getEventCount('page_view');
    expect(count).toBe(5);
  });

  it('should call flush callback after batch size', () => {
    const flushCallback = jasmine.createSpy('flushCallback');
    tracker.onFlush(flushCallback);

    for (let i = 0; i < 10; i++) {
      tracker.track('click', { element: `btn_${i}` });
    }

    expect(flushCallback).toHaveBeenCalledTimes(1);
    expect(flushCallback).toHaveBeenCalledWith(jasmine.arrayContaining([
      jasmine.objectContaining({ type: 'click' }),
    ]));
  });
});
```

### LocalStorage and SessionStorage Testing

```javascript
describe('StorageService', () => {
  let service;

  beforeEach(() => {
    localStorage.clear();
    sessionStorage.clear();
    service = new StorageService();
  });

  afterEach(() => {
    localStorage.clear();
    sessionStorage.clear();
  });

  it('should store and retrieve values', () => {
    service.set('user', { name: 'Alice', role: 'admin' });
    const result = service.get('user');
    expect(result).toEqual({ name: 'Alice', role: 'admin' });
  });

  it('should return null for missing keys', () => {
    expect(service.get('nonexistent')).toBeNull();
  });

  it('should handle storage quota exceeded', () => {
    spyOn(localStorage, 'setItem').and.throwError('QuotaExceededError');
    expect(() => service.set('key', 'value')).toThrowError('Storage quota exceeded');
  });

  it('should clear expired items', () => {
    service.set('temp', 'data', { ttl: -1 }); // Already expired
    service.cleanExpired();
    expect(service.get('temp')).toBeNull();
  });
});
```

## Best Practices

1. **Use headless browsers in CI** -- Configure `ChromeHeadless` or `FirefoxHeadless` for CI pipelines to avoid display server requirements while maintaining real browser engine testing.
2. **Enforce coverage thresholds** -- Set minimum coverage percentages in `coverageReporter.check.global` and fail the build when coverage drops below acceptable levels.
3. **Use `singleRun: true` for CI** -- Ensure Karma exits after tests complete in CI environments. Watch mode (`autoWatch: true`) is for development only.
4. **Clean up DOM after each test** -- Remove any elements added to `document.body` in `afterEach` hooks to prevent test pollution.
5. **Use Jasmine's `createSpyObj`** for creating mock objects with multiple methods. This is cleaner than manually stubbing each method.
6. **Configure appropriate timeouts** -- Set `browserNoActivityTimeout` and `browserDisconnectTimeout` high enough for slow CI environments but low enough to catch hanging tests.
7. **Use preprocessors for modern JavaScript** -- Configure webpack or browserify preprocessors for TypeScript, ES modules, and JSX compilation before test execution.
8. **Separate CI and dev configurations** -- Create `karma.ci.conf.js` that extends the base config with CI-specific settings (headless, single run, junit reporter).
9. **Use source maps** -- Enable source map preprocessor for accurate error stack traces and coverage mapping back to original source files.
10. **Group related specs in `describe` blocks** -- Organize tests hierarchically by module, class, and method for clear reporting output.

## Anti-Patterns

1. **Running headed browsers in CI** -- Using non-headless Chrome/Firefox in CI requires a display server (Xvfb) and is slower. Always use headless variants.
2. **Not cleaning up DOM elements** -- Elements added to `document.body` in tests persist across tests, causing side effects and false positives.
3. **Missing `singleRun` in CI** -- Without `singleRun: true`, Karma watches for changes and never exits, hanging the CI pipeline.
4. **Hardcoding file paths in `files` array** -- Use glob patterns (`src/**/*.js`) instead of listing individual files. New files are automatically included.
5. **Not using coverage thresholds** -- Without thresholds, coverage can silently drop over time. Enforce minimums to maintain quality.
6. **Ignoring browser-specific failures** -- Tests that pass in Chrome but fail in Firefox indicate real compatibility issues. Investigate rather than skip.
7. **Using `fit` and `fdescribe` in committed code** -- Focused tests (`fit`, `fdescribe`) skip other tests silently. Use `--grep` for selective execution instead.
8. **Not configuring preprocessors** -- Serving raw TypeScript or ES modules to browsers causes syntax errors. Always configure appropriate compilation preprocessors.
9. **Setting `browserNoActivityTimeout` too low** -- Tests that involve async operations may need more than the default timeout. Set it to at least 30 seconds for CI.
10. **Not using reporter plugins** -- The default `progress` reporter is insufficient for CI. Add `junit` for CI integration and `coverage` for quality metrics.

## CLI Reference

```bash
# Run tests (uses karma.conf.js by default)
npx karma start

# Run in single-run mode
npx karma start --single-run

# Run with specific config
npx karma start karma.ci.conf.js

# Run with specific browsers
npx karma start --browsers ChromeHeadless,FirefoxHeadless

# Run specific test files
npx karma start --files "test/unit/calculator.spec.js"

# Run with verbose logging
npx karma start --log-level debug

# Initialize karma config
npx karma init

# Watch and re-run on changes
npx karma start --auto-watch --no-single-run
```

## Setup

```bash
# Install Karma and Jasmine
npm install --save-dev karma karma-jasmine karma-chrome-launcher jasmine-core

# Coverage reporting
npm install --save-dev karma-coverage

# CI reporting
npm install --save-dev karma-junit-reporter

# TypeScript support
npm install --save-dev karma-webpack karma-sourcemap-loader typescript ts-loader

# Istanbul coverage for webpack/TypeScript
npm install --save-dev karma-coverage-istanbul-reporter

# Initialize configuration
npx karma init karma.conf.js
```

