Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing to Multiforum

This guide will help you contribute to the Multiforum codebase, with a focus on testing practices.

This guide is for the **frontend** ([multiforum-nuxt](https://github.com/gennit-project/multiforum-nuxt)). The backend lives in [multiforum-backend](https://github.com/gennit-project/multiforum-backend); see its README for backend setup and architecture.

## Prerequisites

- **Node.js 26** is the recommended development runtime (see `.nvmrc`). Node.js
  ≥ 24.13.0 remains supported while Vercel deploys on Node 24. Older majors are
  not supported. If you use `nvm`, run `nvm use` before installing or running
  scripts.
- **npm** (bundled with Node).

## Environment Variables

The application requires several environment variables to be set up. Copy `.env.example` to `.env` and update the values.

### Core Application Variables

| Variable                   | Description                                         |
| -------------------------- | --------------------------------------------------- |
| `GRAPHQL_URL_FOR_TYPES`    | URL for GraphQL schema used in code generation      |
| `VITE_GRAPHQL_URL`         | URL for GraphQL API endpoint                        |
| `VITE_BASE_URL`            | Base URL for the application                        |
| `VITE_SERVER_NAME`         | Internal server name (must match backend config)    |
| `VITE_SERVER_DISPLAY_NAME` | User-facing name of the server                      |
| `VITE_ENVIRONMENT`         | Environment name (development, production, etc.)    |
| `NITRO_PRESET`             | Deployment preset for Nitro server (e.g., 'vercel') |

### Authentication Variables

| Variable                   | Description                          |
| -------------------------- | ------------------------------------ |
| `VITE_AUTH0_DOMAIN`        | Auth0 domain for authentication      |
| `VITE_AUTH0_CLIENT_ID`     | Auth0 application client ID          |
| `VITE_AUTH0_CLIENT_SECRET` | Auth0 application client secret      |
| `VITE_AUTH0_AUDIENCE`      | Auth0 API audience identifier        |
| `VITE_AUTH0_SCOPE`         | Auth0 permission scopes              |
| `VITE_AUTH0_URL`           | Auth0 token endpoint                 |
| `VITE_AUTH0_CALLBACK_URL`  | URL to redirect after authentication |
| `VITE_LOGOUT_URL`          | URL to redirect to after logout      |

### External Services Variables

| Variable                           | Description                                    |
| ---------------------------------- | ---------------------------------------------- |
| `VITE_GOOGLE_MAPS_API_KEY`         | API key for Google Maps integration            |
| `VITE_OPEN_CAGE_API_KEY`           | API key for OpenCage geocoding service         |
| `VITE_OPEN_GRAPH_API_KEY`          | API key for OpenGraph services                 |
| `VITE_GOOGLE_CLOUD_STORAGE_BUCKET` | Google Cloud Storage bucket for file uploads   |
| `VITE_SENTRY_AUTH_TOKEN`           | Sentry authentication token for error tracking |
| `VITE_SENTRY_DSN`                  | Sentry Data Source Name for error reporting    |

### Docker Compose Variables

| Variable                    | Description                                       |
| --------------------------- | ------------------------------------------------- |
| `NEO4J_AUTH`                | Neo4j database authentication (username/password) |
| `NEO4J_USERNAME`            | Neo4j database username                           |
| `NEO4J_PASSWORD`            | Neo4j database password                           |
| `GCS_BUCKET_NAME`           | Google Cloud Storage bucket name                  |
| `GOOGLE_CREDENTIALS_BASE64` | Base64-encoded Google Cloud credentials           |
| `SLACK_WEBHOOK_URL`         | Slack webhook URL for notifications               |
| `AUTH0_DOMAIN`              | Auth0 domain for server-side operations           |
| `AUTH0_CLIENT_ID`           | Auth0 client ID for server-side operations        |
| `SERVER_CONFIG_NAME`        | Server configuration name                         |

### Testing Variables

| Variable                      | Description                                  |
| ----------------------------- | -------------------------------------------- |
| `VITE_AUTH0_USERNAME`         | Test username for Auth0 in integration tests |
| `VITE_AUTH0_PASSWORD`         | Test password for Auth0 in integration tests |
| `VITE_AUTH0_USERNAME_2`       | Secondary test username for Auth0            |
| `VITE_AUTH0_PASSWORD_2`       | Secondary test password for Auth0            |

## Getting Started

1. Clone the repository
2. Select the recommended Node 26 runtime (`nvm use`)
3. Install the repository's pinned pnpm version (Node 26 no longer bundles Corepack): `npm install --global pnpm@10.28.2`
4. Copy `.env.example` to `.env` and update the values
5. Install dependencies with `pnpm install`
6. Start the development server with `pnpm run dev`
7. Run unit tests with `pnpm run test:unit` (Vitest) and end-to-end tests with `pnpm run test:playwright` (Playwright, mocked GraphQL)
8. Before committing, run `pnpm run verify` (type-check + unit tests); the pre-commit hook runs this plus ESLint automatically

## Testing Guidelines

### Testing Patterns

#### Unit Tests (Vitest)

We use Vitest with Vue Test Utils for component and utility testing. Unit tests should:

- Test one specific piece of functionality
- Mock external dependencies
- Focus on behavior, not implementation details
- Be fast and deterministic

**Component Test Structure:**

```typescript
// Import utilities and component
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import YourComponent from '@/components/YourComponent.vue';

describe('YourComponent', () => {
  // Test rendering
  it('renders correctly with default props', () => {
    const wrapper = mount(YourComponent);
    expect(wrapper.exists()).toBe(true);
    expect(wrapper.text()).toContain('Expected Text');
  });

  // Test props
  it('applies custom prop values correctly', () => {
    const wrapper = mount(YourComponent, {
      props: { title: 'Custom Title' },
    });
    expect(wrapper.text()).toContain('Custom Title');
  });

  // Test user interaction
  it('emits an event when button is clicked', async () => {
    const wrapper = mount(YourComponent);
    await wrapper.find('button').trigger('click');
    expect(wrapper.emitted()).toHaveProperty('custom-event');
  });

  // Test conditional rendering
  it('shows error message when validation fails', async () => {
    const wrapper = mount(YourComponent);
    await wrapper.find('input').setValue('invalid');
    await wrapper.find('form').trigger('submit');
    expect(wrapper.text()).toContain('Error message');
  });
});
```

#### End-to-End Tests (Playwright)

End-to-end tests use Playwright with **mocked GraphQL responses** (no backend
required). Tests should:

- Focus on user-centric workflows
- Test feature interactions
- Use `installMockAuth()` for programmatic authentication (faster and more
  reliable than driving the login UI)
- Leverage `data-testid` attributes for stable selectors
- Wait on network responses or UI state instead of arbitrary sleeps

**End-to-End Test Structure:**

```typescript
import { test, expect } from '@playwright/test';
import { installMockAuth } from '../support/auth';

test('completes a user workflow successfully', async ({ page }) => {
  await installMockAuth(page);

  await page.goto('/forums/cats');

  await page.getByTestId('create-discussion-button').click();
  await page.getByTestId('title-input').fill('Test Discussion');
  await page.getByTestId('body-input').fill('Test Body');

  // Wait for the actual GraphQL request to complete instead of a fixed timeout
  await Promise.all([
    page.waitForResponse((res) => res.url().includes('/graphql')),
    page.getByTestId('save-button').click(),
  ]);

  await expect(page.getByTestId('discussion-title')).toContainText(
    'Test Discussion'
  );
});
```

Run a single mocked spec with:

```bash
pnpm run test:playwright:mocked -- tests/playwright/mocked/discussions/createEditDeleteDiscussions.spec.ts
```

### Best Practices

#### Example: Testing a Form Component

```typescript
// FormComponent.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import FormComponent from '@/components/FormComponent.vue';

describe('FormComponent', () => {
  // Setup common test data
  const validInput = {
    name: 'Test User',
    email: 'test@example.com',
  };

  const mountComponent = (props = {}) => {
    return mount(FormComponent, {
      props: {
        initialValues: {},
        ...props,
      },
    });
  };

  it('validates required fields', async () => {
    const wrapper = mountComponent();

    // Submit empty form
    await wrapper.find('form').trigger('submit');

    // Check for validation messages
    expect(wrapper.text()).toContain('Name is required');
    expect(wrapper.text()).toContain('Email is required');
  });

  it('validates email format', async () => {
    const wrapper = mountComponent();

    // Input invalid email
    await wrapper.find('[data-testid="name-input"]').setValue('Test User');
    await wrapper.find('[data-testid="email-input"]').setValue('invalid-email');
    await wrapper.find('form').trigger('submit');

    // Check for validation message
    expect(wrapper.text()).toContain('Email format is invalid');
  });

  it('submits valid data and emits event', async () => {
    const wrapper = mountComponent();

    // Fill form with valid data
    await wrapper.find('[data-testid="name-input"]').setValue(validInput.name);
    await wrapper
      .find('[data-testid="email-input"]')
      .setValue(validInput.email);

    // Submit form
    await wrapper.find('form').trigger('submit');

    // Verify event emission
    expect(wrapper.emitted()).toHaveProperty('submit');
    expect(wrapper.emitted().submit[0][0]).toEqual(validInput);
  });
});
```

#### Example: Testing a Complex Workflow (Playwright)

```typescript
// tests/playwright/mocked/discussions/createEditDeleteDiscussions.spec.ts
import { test, expect } from '@playwright/test';
import { installMockAuth } from '../../support/auth';

test('creates, edits, and deletes a discussion', async ({ page }) => {
  await installMockAuth(page);

  // Create
  await page.goto('/forums/cats/discussions/create');
  await page.getByTestId('title-input').fill('Test Discussion');
  await page.getByTestId('body-input').fill('Initial content');
  await Promise.all([
    page.waitForResponse((res) => res.url().includes('/graphql')),
    page.getByTestId('save-button').click(),
  ]);
  await expect(page.getByTestId('discussion-title')).toContainText(
    'Test Discussion'
  );

  // Edit
  await page.getByTestId('discussion-menu-button').click();
  await page.getByTestId('discussion-menu-button-item-Edit').click();
  await page.getByTestId('body-input').fill('Updated content');
  await Promise.all([
    page.waitForResponse((res) => res.url().includes('/graphql')),
    page.getByTestId('save-button').click(),
  ]);
  await expect(page.getByTestId('discussion-body')).toContainText(
    'Updated content'
  );

  // Delete
  await page.getByTestId('discussion-menu-button').click();
  await page.getByTestId('discussion-menu-button-item-Delete').click();
  await page.getByTestId('confirm-delete-button').click();
  await expect(page).toHaveURL(/\/forums\/cats\/discussions/);
});
```

### Common Testing Pitfalls to Avoid

1. **Flaky Tests**
   - ❌ Don't use arbitrary timeouts: `await page.waitForTimeout(3000)`
   - ✅ Do wait for the actual network response or UI state: `page.waitForResponse(...)`, `await expect(locator).toBeVisible()`

2. **Brittle Selectors**
   - ❌ Don't use CSS classes that might change: `page.locator('.button-primary')`
   - ✅ Do use test ids: `page.getByTestId('save-button')`

3. **Testing Implementation Details**
   - ❌ Don't test internal component state: `expect(wrapper.vm.internalValue).toBe(true)`
   - ✅ Do test observable behavior: `expect(wrapper.text()).toContain('Error message')`

4. **Brittle Authentication Setup**
   - ❌ Don't drive the login UI in every test
   - ✅ Do authenticate programmatically with `installMockAuth(page)`

5. **Missing Network Request Handling**
   - ❌ Don't assume requests complete instantly
   - ✅ Do wait for the GraphQL response alongside the action that triggers it:
     ```typescript
     await Promise.all([
       page.waitForResponse((res) => res.url().includes('/graphql')),
       page.getByTestId('save-button').click(),
     ]);
     ```

6. **Overly Coupled Tests**
   - ❌ Don't create tests that depend on each other's state
   - ✅ Do create independent test cases with clear setup/teardown

7. **Testing Too Much in One Test**
   - ❌ Don't write lengthy tests covering multiple behaviors
   - ✅ Do create focused tests with clear assertions:
     ```typescript
     // Instead of one large test:
     it('validates email field');
     it('validates password field');
     it('allows submission with valid data');
     ```

8. **Inconsistent Mocking**
   - ❌ Don't partially mock dependencies
   - ✅ Do completely mock external dependencies or use real implementations

### Frontend testing patterns

These conventions keep tests fast, meaningful, and consistent with the rest of
the suite.

- **Colocate specs.** Put a unit spec next to the file it tests
  (`Foo.vue` → `Foo.spec.ts`, `foo.ts` → `foo.spec.ts`). Specs under `tests/unit`
  are also picked up.
- **Test real logic, not reimplementations.** When a component holds non-trivial
  logic (formatting, validation, derivation), extract it into a `utils/` function
  and unit-test that function directly. This is how the large components
  (`CreateEditEventFields`, `IssueDetail`, `CommentSection`, `Comment`) are
  tested — the pure rules live in tested utils and the component just calls them.
- **Test pages through their child components.** Most pages are thin wrappers.
  Use `shallowMount` + `findComponent(Child)` and assert the props the page
  passes down (or the events it forwards), rather than rendering the whole tree:

  ```typescript
  const search = shallowMount(DiscussionsIndexPage, {
    global: { stubs: { NuxtLayout: SlotRenderingStub } },
  }).findComponent(SearchDiscussions);
  expect(search.props('isForumScoped')).toBe(false);
  ```

  - Stub layout/slot wrappers (`NuxtLayout`, `FormRow`) with a component that
    renders the relevant slot, otherwise `shallowMount` swallows slot content.
  - For pages that call `definePageMeta`, add `vi.stubGlobal('definePageMeta', vi.fn())`.
- **Keep `vi.mock` at the module top level.** Vitest hoists `vi.mock` to the top
  of the file, so calling it inside `beforeEach`/`describe` is misleading and now
  triggers a deprecation warning. If the mock factory references local variables,
  prefix their names with `mock` (e.g. `mockThemeRef`) so Vitest allows the
  hoisted reference.
- **One assertion per `it`.** Prefer multiple focused `it` blocks or a single
  structured `expect` over several assertions in one test.
- **Mount the actual component, never a hand-written stand-in.** A spec that
  defines `const FooTest = { name: 'Foo', template: '...' }` and mounts *that*
  contributes **zero** coverage to `Foo.vue` and silently drifts from the real
  component. If a component is hard to mount, mock its dependencies (below) — do
  not reimplement it in the spec. (`CommentSection.spec.ts` was rewritten for
  exactly this reason.)
- **Mock Apollo at the module level for components that use it.** `mountWithDefaults`
  does not wire Apollo, so mock `@vue/apollo-composable` in the spec. To exercise
  a component's mutation callbacks, return a `mutate()` that synchronously fires
  its registered `onDone` handlers; return an empty `useQuery` result so
  query-backed composables resolve to their default state:

  ```typescript
  vi.mock('@vue/apollo-composable', async () => {
    const { ref } = await import('vue');
    return {
      useMutation: () => {
        const done: Array<(p: unknown) => void> = [];
        return {
          mutate: vi.fn(() => { done.forEach((cb) => cb({ data: {} })); }),
          onDone: (cb: (p: unknown) => void) => done.push(cb),
          onError: vi.fn(),
          loading: ref(false),
          error: ref(null),
        };
      },
      useQuery: () => ({ result: ref(null), loading: ref(false), error: ref(null), onResult: vi.fn(), onError: vi.fn() }),
    };
  });
  ```

- **Seed hoisted mock factories with the *full* return shape.** A mock typed by
  its initial implementation narrows the return type, so a later
  `mockReturnValue` with extra fields fails the typecheck. CI's `vue-tsc` job
  catches this even when the test passes locally, so seed every field up front:

  ```typescript
  // ❌ inferred return type is { valid: boolean } — a later
  //    mockReturnValue({ valid: false, message }) fails vue-tsc (TS2353)
  const isFileSizeValid = vi.fn(() => ({ valid: true }));
  // ✅ seed the full shape
  const isFileSizeValid = vi.fn(() => ({ valid: true, message: '' }));
  ```

- **`@/utils` and `@/utils/index` are the same module.** If you `vi.mock` one,
  mock both with every export the code under test imports — otherwise the second
  mock silently overrides the first and drops exports.
- **Teleported UI renders into `document.body`, not the wrapper.** Tooltips and
  modals using `<Teleport to="body">` are outside the component subtree; assert
  with `document.body.querySelector(...)` rather than `wrapper.find(...)`.
- **Mocked GraphQL entities need `__typename`.** Apollo normalizes by
  `__typename` + key field; a mock entity without `__typename` reaches the
  component as just its key field (e.g. a channel arrives with only `uniqueName`).
  This applies to both Apollo unit mocks and the Playwright GraphQL fixtures.

## Additional Resources

- [Vitest Documentation](https://vitest.dev/)
- [Vue Test Utils Documentation](https://test-utils.vuejs.org/)
- [Playwright Documentation](https://playwright.dev/)
- Check out existing tests in the codebase for practical examples