# How Tests Are Organized in the open-seo Project: Co-Located Unit Tests and Dedicated E2E Suites

> Discover how open-seo organizes tests placing unit tests with source files and dedicated e2e suites in the e2e folder using Vitest and Playwright.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-06-28

---

**The open-seo project organizes tests using a co-location strategy where unit and integration tests (*.test.ts) live alongside source files in src/, while end-to-end tests (*.spec.ts) reside in a dedicated e2e/ folder, powered by Vitest and Playwright respectively.**

Understanding how tests are organized in the every-app/open-seo repository is essential for contributing effectively. The codebase maintains a clear separation between fast unit checks and comprehensive browser automation, with specific naming conventions and directory structures that scale with the application's growth.

## Unit and Integration Tests: Co-Located with Source Code

The project places **unit and integration tests** directly next to the modules they verify, using Vitest as the test runner to ensure fast feedback during development.

### File Pattern and Location

Tests use the `*.test.ts` naming convention and mirror the production folder hierarchy inside `src/`. When you open a source file, its corresponding test file sits in the same directory. For example, the MCP transport layer implementation at [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) has its corresponding test file at [`src/server/mcp/transport.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.test.ts). Similarly, business-logic services like [`src/server/features/keywords/services/research/saved-keywords.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/keywords/services/research/saved-keywords.test.ts) and low-level utilities such as [`src/server/lib/audit/url-utils.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/url-utils.test.ts) follow this pattern, as do React components and server helpers throughout the tree.

### Framework and Execution

**Vitest** provides a Jest-compatible API (`describe`, `it`, `expect`) running in a Node.js environment. This setup validates pure-logic functions, database helpers, and API utilities without spinning up a browser. Execute the suite with:

```bash

# Run all unit tests once

pnpm test

# Watch mode for development

pnpm test:watch

```

Co-location keeps tests discoverable—developers see specifications immediately when navigating to a module—allowing the repository to scale without a massive, flat `tests/` directory at the root.

## End-to-End Tests: Browser Automation in e2e/

For **full-stack validation**, open-seo uses Playwright specifications stored in a top-level `e2e/` folder, separated from the unit test suite to reflect their broader scope.

### Directory Structure and File Pattern

E2E tests use the `*.spec.ts` pattern and reside exclusively under `e2e/`. Examples include [`e2e/keyword-research-navigation.spec.ts`](https://github.com/every-app/open-seo/blob/main/e2e/keyword-research-navigation.spec.ts) for keyword research user flows and [`e2e/domain-overview-filters.spec.ts`](https://github.com/every-app/open-seo/blob/main/e2e/domain-overview-filters.spec.ts) for domain analytics validation. Performance-focused variants like [`domain-overview-filters.perf.spec.ts`](https://github.com/every-app/open-seo/blob/main/domain-overview-filters.perf.spec.ts) also live here, asserting timing thresholds under realistic conditions.

### Framework and Execution

**Playwright** handles browser automation and assertions, configured via [`playwright.config.ts`](https://github.com/every-app/open-seo/blob/main/playwright.config.ts) at the repository root. These tests spin up the entire application (via `wrangler dev`) to exercise user-facing interactions, including navigation, filtering, and data fetching with mocked DataForSEO responses.

Run E2E tests selectively or in full:

```bash

# Run all E2E specs

pnpm test:e2e

# Run only the domain overview filter suite

pnpm test:e2e:domain

# Run performance-specific tests

pnpm test:e2e:domain:perf

```

## Test Scripts and Configuration

The `scripts` section in [`package.json`](https://github.com/every-app/open-seo/blob/main/package.json) defines clear entry points for both test layers, enabling straightforward CI integration and local development:

```json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ci": "vitest run --reporter=dot",
    "test:e2e": "playwright test",
    "test:e2e:domain": "playwright test e2e/domain-overview-filters.spec.ts",
    "test:e2e:domain:perf": "playwright test e2e/domain-overview-filters.perf.spec.ts",
    "test:e2e:keywords": "playwright test e2e/keyword-research-navigation.spec.ts"
  }
}

```

The CI pipeline defined in [`.github/workflows/ci.yml`](https://github.com/every-app/open-seo/blob/main/.github/workflows/ci.yml) invokes these scripts to ensure both Vitest and Playwright suites pass on every pull request merge.

## Adding New Tests to the Codebase

### Creating a Unit Test

When adding a utility like [`src/server/lib/audit/url-utils.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/url-utils.ts), create a sibling file:

```typescript
// src/server/lib/audit/url-utils.test.ts
import { normalizeUrl } from './url-utils';

describe('normalizeUrl', () => {
  it('removes trailing slashes', () => {
    expect(normalizeUrl('https://example.com/')).toBe('https://example.com');
  });
});

```

Vitest discovers this file automatically via the `*.test.ts` pattern when running `pnpm test`.

### Creating an E2E Scenario

For new dashboard functionality, add a Playwright spec:

```typescript
// e2e/dashboard-analytics.spec.ts
import { test, expect } from '@playwright/test';

test('dashboard displays analytics chart', async ({ page }) => {
  await page.goto('http://localhost:3000/dashboard');
  await expect(page.locator('#analytics-chart')).toBeVisible();
});

```

Execute with `pnpm test:e2e` to include the new spec in the full browser automation suite.

## Summary

- **Co-location strategy**: Unit tests (`*.test.ts`) live next to source files in `src/`, while E2E tests (`*.spec.ts`) reside in `e2e/`.
- **Vitest powers unit tests**: Provides fast, Jest-compatible execution for server utilities, React components, and business logic located alongside the code they verify.
- **Playwright handles E2E**: Manages browser automation for full-stack user flows like keyword research navigation and domain overview filtering, spinning up the full application via `wrangler dev`.
- **NPM scripts unify execution**: `pnpm test` runs Vitest, `pnpm test:e2e` runs Playwright, with granular scripts available for specific suites such as `test:e2e:domain`.
- **Key configuration files**: [`package.json`](https://github.com/every-app/open-seo/blob/main/package.json) defines scripts, [`playwright.config.ts`](https://github.com/every-app/open-seo/blob/main/playwright.config.ts) configures E2E settings, and [`.github/workflows/ci.yml`](https://github.com/every-app/open-seo/blob/main/.github/workflows/ci.yml) orchestrates continuous integration for both test layers.

## Frequently Asked Questions

### What testing frameworks does open-seo use?

The project uses **Vitest** for unit and integration tests and **Playwright** for end-to-end browser automation. Vitest runs in a Node.js environment with a Jest-compatible API, while Playwright executes real browser scenarios against a running instance of the application.

### Why are unit tests co-located with source files instead of in a separate tests/ directory?

Co-location improves discoverability and maintainability. When developers open [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts), they immediately see [`transport.test.ts`](https://github.com/every-app/open-seo/blob/main/transport.test.ts) alongside it. This pattern scales naturally as the codebase grows, preventing a massive, unwieldy top-level `tests/` directory and ensuring tests are grouped by feature rather than separated by file type.

### How do I run only the domain overview filter tests?

Use the specific npm script defined in [`package.json`](https://github.com/every-app/open-seo/blob/main/package.json):

```bash
pnpm test:e2e:domain

```

This command invokes `playwright test e2e/domain-overview-filters.spec.ts` directly, targeting only that specification file rather than the entire E2E suite.

### Where are the Playwright configuration and CI workflows defined?

Playwright configuration lives in [`playwright.config.ts`](https://github.com/every-app/open-seo/blob/main/playwright.config.ts) at the repository root. Continuous integration workflows that execute both Vitest and Playwright suites are defined in [`.github/workflows/ci.yml`](https://github.com/every-app/open-seo/blob/main/.github/workflows/ci.yml), ensuring all tests pass before merging pull requests.