# Testing Patterns

**Analysis Date:** 2026-01-11

## Test Framework

**Runner:**
- Playwright 1.40.0 - E2E testing for React app (`jam-ui/package.json`, `jam-ui/playwright.config.ts`)
- Jest - Unit testing for React (inferred from test file patterns)
- RSpec 2.11 - Ruby testing framework (`ruby/Gemfile`)
- Jasmine - Legacy JavaScript tests with Karma runner (`web/spec/javascripts/karma.conf.js`)

**Assertion Library:**
- Jest `expect` - Unit tests
- Playwright assertions - E2E tests (`expect(page).toHaveText(...)`)
- RSpec expectations - Ruby tests (`.should`, `expect { }.to`)

**Run Commands:**
```bash
# React tests
cd jam-ui
npm test                              # Playwright E2E tests
npx cypress run                       # Cypress tests (alternative)

# Ruby tests
cd ruby
bundle exec rspec                     # Ruby gem tests

cd web
bundle exec rspec                     # Rails backend tests

cd websocket-gateway
bundle exec rspec                     # WebSocket gateway tests
```

## Test File Organization

**Location:**
- React unit tests: `jam-ui/tests/helpers/rest.test.js` (co-located pattern)
- Playwright E2E: `jam-ui/test/friends.spec.ts` (TypeScript)
- Legacy JavaScript: `web/spec/javascripts/*.spec.js`
- Ruby tests: `ruby/spec/jam_ruby/models/*.spec.rb`, `web/spec/controllers/*.spec.rb`

**Naming:**
- Unit tests: `{module}.test.js` (e.g., `rest.test.js`)
- E2E tests: `{feature}.spec.ts` (e.g., `friends.spec.ts`)
- Ruby tests: `{class_name}_spec.rb` (e.g., `text_message_spec.rb`)

**Structure:**
```
jam-ui/
  tests/helpers/rest.test.js          # Jest unit tests
  test/friends.spec.ts                # Playwright E2E
web/
  spec/
    controllers/                      # Controller specs
    javascripts/                      # Legacy Jasmine tests
      karma.conf.js                   # Karma configuration
ruby/
  spec/jam_ruby/models/              # Model specs
```

## Test Structure

**Jest Unit Tests:**
```javascript
// jam-ui/tests/helpers/rest.test.js
describe('rest helpers', () => {
  test('getPersonById calls correct URL', async () => {
    const payload = { id: 42, name: 'John' };
    mockResolved(payload);
    const result = await rest.getPersonById(42);
    expectCalledWith('/users/42/profile');
    expect(result).toEqual(payload);
  });
});
```

**Playwright E2E Tests:**
```typescript
// jam-ui/test/friends.spec.ts
import { test, expect } from '@playwright/test';

test.describe.serial('Friends page', () => {
  test.use({ storageState: 'test/storageState/user1.json' });

  test('Homepage', async ({ page }) => {
    await page.goto('/');
    await expect(page.locator('h1').first()).toHaveText('Dashboard - Home');
  });
});
```

**RSpec Ruby Tests:**
```ruby
# ruby/spec/jam_ruby/models/text_message_spec.rb
describe TextMessage do
  before do
    TextMessage.delete_all
    @target_user = FactoryGirl.create(:user)
    @msg = TextMessage.new(:target_user_id => @target_user.id)
  end

  it "should retrieve conversation for both users" do
    @msg.message = "Test message"
    @msg.save!
    messages = TextMessage.index(@target_user.id, @source_user.id)
    messages.count.should == 1
  end
end
```

**Jasmine Legacy Tests:**
```javascript
// web/spec/javascripts/jamserver.spec.js
describe("JamServer", function() {
  beforeEach(function() {
    jamserver = context.JK.JamServer;
  });

  it("Subscribing to ping should call function", function() {
    var called = false;
    jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {
      called = true;
    });
    expect(called).toBeTruthy();
  });
});
```

**Patterns:**
- Jest: `describe()` blocks for grouping, `test()` or `it()` for individual tests
- Playwright: `test.describe()` for grouping, `test()` for individual E2E scenarios
- RSpec: `describe` blocks, `before` hooks, `it` blocks with expectations
- Jasmine: `describe()` and `it()` blocks, `beforeEach()` hooks

## Mocking

**Framework:**
- Jest built-in mocking: `jest.mock()` for module mocking
- RSpec: FactoryGirl for test data fixtures
- Manual mocks in test helpers

**Jest Patterns:**
```javascript
// jam-ui/tests/helpers/rest.test.js
// Mock apiFetch module
const mockResolved = (payload) => {
  apiFetch.mockResolvedValue(payload);
};

const expectCalledWith = (url) => {
  expect(apiFetch).toHaveBeenCalledWith(url);
};
```

**RSpec Patterns:**
```ruby
# ruby/spec/jam_ruby/models/text_message_spec.rb
before do
  @target_user = FactoryGirl.create(:user)
  @source_user = FactoryGirl.create(:user)
end
```

**What to Mock:**
- External API calls (via `apiFetch()` in frontend)
- Database queries (via FactoryGirl fixtures in Ruby)
- Time/dates (when testing time-sensitive logic)
- WebSocket connections (in unit tests)

**What NOT to Mock:**
- Pure functions and utilities
- Internal business logic
- Simple data transformations

## Fixtures and Factories

**Test Data:**
- FactoryGirl for Ruby models: `FactoryGirl.create(:user)` in `ruby/spec/`
- Manual test helpers in JavaScript: `mockResolved()`, `expectCalledWith()` in `jam-ui/tests/helpers/rest.test.js`
- Playwright storage state: `test.use({ storageState: 'test/storageState/user1.json' })` for authenticated tests

**Location:**
- JavaScript helpers: `jam-ui/tests/helpers/` (if applicable)
- Ruby factories: Defined in `ruby/spec/` or `web/spec/` (FactoryGirl configuration)
- Playwright fixtures: `jam-ui/test/storageState/` (session cookies for auth)

## Coverage

**Requirements:**
- No enforced coverage targets
- Coverage tracked for awareness, not enforcement
- Focus on critical paths (API endpoints, core models, business logic)

**Configuration:**
- Jest: Built-in coverage via `--coverage` flag
- RSpec: SimpleCov gem for Ruby code coverage
- Playwright: No coverage (E2E tests focus on integration, not coverage)

**View Coverage:**
```bash
# Jest coverage (if configured)
cd jam-ui
npm test -- --coverage

# RSpec coverage
cd ruby
bundle exec rspec
# Check coverage/index.html
```

## Test Types

**Unit Tests:**
- Scope: Single function/module in isolation
- Mocking: Mock all external dependencies (API, database)
- Speed: Fast (<100ms per test)
- Examples: `jam-ui/tests/helpers/rest.test.js`, `ruby/spec/jam_ruby/models/*.spec.rb`

**Integration Tests:**
- Scope: Multiple modules working together
- Mocking: Mock only external boundaries (network, file system)
- Examples: Rails controller specs testing controller + model interaction

**E2E Tests:**
- Framework: Playwright 1.40.0 (primary), Cypress (alternative mentioned in CLAUDE.md)
- Scope: Full user flows through UI
- Setup: Uses storage state for authenticated sessions
- Location: `jam-ui/test/*.spec.ts`

## Common Patterns

**Async Testing:**
```javascript
// Jest async/await
test('handles async operation', async () => {
  const result = await asyncFunction();
  expect(result).toBe('expected');
});

// Playwright async
test('async page interaction', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('h1')).toHaveText('Title');
});
```

**Error Testing:**
```javascript
// Jest error testing
test('throws on invalid input', () => {
  expect(() => functionCall()).toThrow('error message');
});

// Async error
test('rejects on failure', async () => {
  await expect(asyncCall()).rejects.toThrow('error message');
});
```

**Before/After Hooks:**
- Jest: `beforeEach()`, `afterEach()` for setup/teardown
- RSpec: `before do ... end`, `after do ... end`
- Playwright: `test.beforeAll()`, `test.afterAll()` for global setup

**Snapshot Testing:**
- Not used in this codebase
- Prefer explicit assertions for clarity

---

*Testing analysis: 2026-01-11*
*Update when test patterns change*
