# Infraestructura QA -- EnterFarmaPlus

## Resumen

El proyecto cuenta con **5 capas de pruebas** que se ejecutan tanto localmente como en CI/CD (GitHub Actions). Todas las pruebas corren contra un **tenant real** (no usan BD local ni seeders), lo que permite detectar regresiones en el entorno mas cercano a produccion.

| Capa | Tecnologia | Directorio | Que valida |
|------|-----------|------------|------------|
| API Regression | Node.js + axios | `scripts/qa/` | Endpoints API REST y rutas web criticas |
| E2E | Playwright (Chromium) | `tests/e2e/` | Flujos completos en navegador |
| Frontend Unit | Jest + Vue Test Utils | `tests/jest/` | Logica de componentes Vue 2 |
| Backend Feature | PHPUnit + Guzzle | `tests/Feature/` | Endpoints HTTP, validaciones, edge cases |
| Static Analysis | PHPStan/Larastan (nivel 5) | `app/`, `modules/` | Errores de tipos y bugs potenciales |

Adicionalmente existe un **Encoding Guard** (`qa:encoding`) que detecta mojibake y problemas de codificacion UTF-8 en el codigo fuente.

---

## Suites de pruebas

### 1. API Regression (Node.js + axios)

**Archivo principal:** `scripts/qa/run_api_regression.js`

Ejecuta requests HTTP directos contra la API REST y rutas web del tenant. Cubre:

- **Smoke GET API:** `/api/company`, `/api/customers`, `/api/items`, `/api/documents/lists`, etc.
- **Smoke Web:** `/records`, `/tables`, `/columns` de las rutas internas
- **Flujos de creacion:** Creacion de clientes, documentos y otros recursos via POST
- **Busqueda:** Endpoints de busqueda y filtrado
- **Endpoints auxiliares:** Series, tablas, metodos de pago

Genera un reporte JSON en `storage/logs/api_regression_YYYYMMDD_HHmmss.json`.

```bash
npm run qa:api
```

### 2. E2E (Playwright)

**Directorio:** `tests/e2e/`
**Config:** `playwright.config.js`

Pruebas end-to-end con navegador Chromium headless. Cada spec autentica la sesion via `bootAuthenticated()` (login por request HTTP + inyeccion de token).

**Specs disponibles:**

| Archivo | Que prueba |
|---------|-----------|
| `page-smoke.spec.js` | Navegacion a 22+ paginas (Dashboard, Documents, POS, Items, etc.) sin errores 500 ni JS fatales |
| `core-flows.spec.js` | Flujos criticos: crear cliente, crear documento, abrir caja, crear gasto |
| `crud-persons.spec.js` | CRUD completo de clientes/proveedores |
| `crud-items.spec.js` | CRUD de productos |
| `crud-documents.spec.js` | CRUD de documentos de venta |
| `pos-flow.spec.js` | Flujo completo del Punto de Venta |
| `cash-flow.spec.js` | Apertura/cierre de caja |

**Helpers compartidos:** `tests/e2e/helpers.js` -- contiene `attachConsoleCollector()`, `bootAuthenticated()`, `getApiToken()`, `getCsrfFromPage()`.

**Configuracion Playwright:**

- Timeout global: 120s
- Timeout de expect: 15s
- Viewport: 1366x900
- Artefactos en fallo: trace, screenshot y video en `.runtime/playwright/test-results/`

```bash
# Suite completa
npm run qa:e2e

# Modo estricto (sin filtros de ruido -- detecta errores intermitentes)
npm run qa:e2e:strict

# Suites parciales
npm run qa:e2e:smoke    # Solo page-smoke.spec.js
npm run qa:e2e:crud     # Solo crud-*.spec.js
npm run qa:e2e:pos      # Solo pos-flow.spec.js
```

**Modo estricto vs normal:**

`qa:e2e:strict` activa `E2E_STRICT=1`, que deshabilita los filtros que ignoran errores transitorios (debugbar 500, heartbeat, favicon 404, jQuery/PhpDebugBar undefined). Util para auditar antes de un release.

### 3. Frontend Unit (Jest)

**Directorio:** `tests/jest/components/`
**Config:** `jest.config.cjs`

Pruebas unitarias de logica de componentes Vue 2. No montan el DOM completo: instancian `Component.data()` y llaman metodos directamente, mockeando `$http`, `$eventHub`, etc.

**Componentes cubiertos:**

| Spec | Componente |
|------|-----------|
| `DataTable.spec.js` | `DataTable.vue` -- paginacion, filtrado, getRecords |
| `PersonForm.spec.js` | `persons/form.vue` -- validaciones CRUD personas |
| `ItemForm.spec.js` | `items/form.vue` -- validaciones CRUD productos |
| `PosIndex.spec.js` | `pos/index.vue` -- calculos POS, descuentos |
| `DocumentInvoice.spec.js` | `documents/invoice.vue` -- logica de facturacion |
| `dialog_header_menu.spec.js` | `dialog_header_menu.vue` -- menu de cabecera |
| `charge_discounts_form.spec.js` | `charge_discounts/form.vue` -- cargos y descuentos |

**Setup:** `tests/jest/setup.js` polyfilla `matchMedia` para jsdom.
**Mocks:** `tests/jest/__mocks__/` contiene mocks de `canvas`, `query-string`, `print-js`, `ckeditor`, `vue-ckeditor5` y estilos CSS/SCSS.

```bash
npm run test:unit:frontend
```

### 4. Backend Feature (PHPUnit + Guzzle)

**Directorio:** `tests/Feature/`
**Config:** `phpunit.xml` (testsuite `Feature`)

Pruebas HTTP reales (no usan DB testing local) contra el tenant QA via Guzzle. Cada clase implementa su propia carga de `.env.qa` y resolucion de aliases.

| Archivo | Que prueba |
|---------|-----------|
| `EndpointRegressionTest.php` | GET a 10+ endpoints API, verifica status 200 y estructura JSON |
| `ValidationRulesTest.php` | POST con datos invalidos, espera errores de validacion (422 o JSON con `success: false`) |
| `EdgeCaseTest.php` | Requests sin token (401), SQL injection payloads, parametros malformados |
| `PurchaseXmlImportConfirmTest.php` | Confirma importacion XML a nivel servicio con tenancy explicito y rollback en `tenant` |

```bash
# Ejecutar solo el smoke de endpoints
npm run test:feature:api

# Ejecutar la suite backend QA completa
npm run test:feature:qa

# Ejecutar todos los Feature tests (incluye ExampleTest)
php vendor/bin/phpunit --testsuite Feature

# Solo un test especifico
php vendor/bin/phpunit --filter EdgeCaseTest
```

### 5. Static Analysis (PHPStan)

**Config:** `phpstan.neon`
**Nivel:** 5
**Paths:** `app/`, `modules/`

Analiza el codigo PHP buscando errores de tipos, metodos indefinidos, parametros incorrectos, etc.

**Baseline:** `phpstan-baseline.neon` (~46,000 lineas) -- contiene errores preexistentes que se ignoran. Solo errores **nuevos** rompen el build.

**Exclusiones:**

- `app/Tenancy/` (capa custom en desarrollo)
- `modules/*/node_modules/` y `modules/*/vendor/`
- Archivos `.blade.php`

**Errores ignorados globalmente:**

- Metodos magicos de Eloquent Builder (`->with()`, etc.)
- `missingType.iterableValue` y `missingType.generics`

```bash
npm run qa:phpstan

# Con mas memoria si es necesario
php vendor/bin/phpstan analyse --memory-limit=2G
```

---

## Scripts NPM

| Script | Comando real | Descripcion |
|--------|-------------|-------------|
| `qa:api` | `node scripts/qa/run_api_regression.js` | Regresion de endpoints API |
| `qa:e2e` | `npx playwright test` | Suite E2E completa |
| `qa:e2e:strict` | `E2E_STRICT=1 npx playwright test` | E2E sin filtros de ruido |
| `qa:e2e:smoke` | `npx playwright test page-smoke.spec.js` | Solo smoke de navegacion |
| `qa:e2e:crud` | `npx playwright test crud-*.spec.js` | Solo tests CRUD |
| `qa:e2e:pos` | `npx playwright test pos-flow.spec.js` | Solo flujo POS |
| `test:unit:frontend` | `jest --config jest.config.cjs --runInBand` | Tests unitarios Jest |
| `test:feature:api` | `phpunit --filter EndpointRegressionTest` | PHPUnit EndpointRegression |
| `test:feature:qa` | `phpunit --filter "(EndpointRegressionTest|ValidationRulesTest|EdgeCaseTest|PurchaseXmlImportConfirmTest)"` | Suite backend QA completa |
| `qa:phpstan` | `phpstan analyse --memory-limit=1G` | Analisis estatico |
| `qa:encoding` | `node scripts/qa/check_encoding_mojibake.js` | Detectar problemas de encoding |
| `qa:all` | `qa:api` + `qa:e2e` + `test:unit:frontend` | Suite rapida (sin PHP) |
| `qa:all:full` | `qa:all` + `test:feature:qa` | Suite rapida + backend QA |
| `qa:full` | `qa:api` + `qa:e2e` + Jest + backend QA | Suite completa |

---

## Configuracion del entorno

### Variables requeridas

| Variable | Descripcion | Ejemplo |
|----------|-------------|---------|
| `QA_BASE_URL` | URL del tenant de pruebas | `http://demo.enterfarmaplus.test` |
| `QA_API_TOKEN` | Token de autenticacion API (opcion 1) | `eyJ0eXAi...` |
| `QA_API_EMAIL` | Email para auth API (opcion 2) | `admin@gmail.com` |
| `QA_API_PASSWORD` | Password para auth API (opcion 2) | `123456` |
| `QA_WEB_EMAIL` | Email para login web (E2E y smoke) | `admin@gmail.com` |
| `QA_WEB_PASSWORD` | Password para login web | `123456` |
| `QA_ENABLED_MODULES` | Lista opcional de modulos para exigir en smoke HTTP | `MobileApp,Inventory` |
| `REQUEST_TIMEOUT_MS` | Timeout de requests en ms (opcional) | `30000` |

### Crear `.env.qa` para ejecucion local

```bash
cp .env.qa.example .env.qa
# Editar con tus credenciales del tenant de pruebas
```

### Orden de carga de variables

El helper `scripts/qa/load_qa_env.js` carga variables en este orden (primera definicion gana):

1. Variables de entorno del sistema (CI inyecta aqui)
2. `.env.qa.local` (credenciales personales, en `.gitignore`)
3. `.env.qa` (valores compartidos del equipo)
4. `.env` (fallback general)

### Aliases automaticos

Las variables con prefijo `QA_` se mapean a sus equivalentes sin prefijo:

```
QA_BASE_URL    → BASE_URL
QA_API_TOKEN   → API_TOKEN
QA_API_EMAIL   → API_EMAIL
QA_API_PASSWORD → API_PASSWORD
QA_WEB_EMAIL   → WEB_EMAIL
QA_WEB_PASSWORD → WEB_PASSWORD
```

Si `API_EMAIL/API_PASSWORD` no estan definidos, se usan `WEB_EMAIL/WEB_PASSWORD` como fallback. Tambien se generan automaticamente `E2E_API_TOKEN`, `E2E_EMAIL` y `E2E_PASSWORD` para Playwright.

### Configuracion en CI (GitHub Actions)

En GitHub Actions, las variables se definen como **repository secrets**:

- `QA_BASE_URL`
- `QA_API_TOKEN`
- `QA_API_EMAIL`
- `QA_API_PASSWORD`
- `QA_WEB_EMAIL`
- `QA_WEB_PASSWORD`

El workflow `.github/workflows/qa.yml` los mapea al entorno en la seccion `env:` global.

---

## Agregar nuevas pruebas

### Nuevo endpoint en API Regression

Editar `scripts/qa/run_api_regression.js`. Agregar un check dentro de la funcion `main()`:

```javascript
// Dentro de la seccion de smoke checks
try {
  const res = await api.get('/api/mi-nuevo-endpoint', {
    headers: { Authorization: `Bearer ${token}` },
  });
  pushCheck('GET /api/mi-nuevo-endpoint', res.status === 200, {
    status: res.status,
  });
} catch (err) {
  pushCheck('GET /api/mi-nuevo-endpoint', false, {
    message: err.message,
  });
}
```

### Nuevo spec E2E (Playwright)

Crear `tests/e2e/mi-flujo.spec.js`:

```javascript
const { test, expect } = require('@playwright/test');
const { attachConsoleCollector, bootAuthenticated } = require('./helpers');

test.describe('Mi nuevo flujo', () => {
  test('descripcion del caso', async ({ page, context }) => {
    const errors = attachConsoleCollector(page);
    await bootAuthenticated(page, context);

    // Navegar a la pagina
    await page.goto('/mi-pagina', { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(2000);

    // Verificar contenido
    const bodyText = await page.textContent('body');
    expect(bodyText).not.toContain('Whoops');

    // Interactuar con elementos
    await page.fill('input[name="campo"]', 'valor');
    await page.click('button:has-text("Guardar")');

    // Verificar que no hubo errores fatales
    const fatal = errors.filter((e) => e.startsWith('pageerror:'));
    expect(fatal).toEqual([]);
  });
});
```

Ejecutar solo el nuevo spec:

```bash
npx playwright test tests/e2e/mi-flujo.spec.js --config=playwright.config.js
```

### Nuevo test Jest (componente Vue)

Crear `tests/jest/components/MiComponente.spec.js`:

```javascript
const Component = require('../../../resources/js/views/tenant/mi-modulo/MiComponente.vue').default;

function createVm(overrides = {}) {
  const vm = Component.data();

  // Mockear dependencias
  vm.$http = {
    get: jest.fn().mockResolvedValue({ data: { data: [], meta: {} } }),
    post: jest.fn().mockResolvedValue({ data: { success: true } }),
  };
  vm.$eventHub = { $on: jest.fn(), $off: jest.fn() };

  // Bindear metodos del componente
  Object.keys(Component.methods || {}).forEach((name) => {
    vm[name] = Component.methods[name].bind(vm);
  });

  return vm;
}

describe('MiComponente.vue (logic)', () => {
  test('metodo X retorna valor esperado', () => {
    const vm = createVm();
    const result = vm.miMetodo();
    expect(result).toBe('valor esperado');
  });

  test('metodo Y llama a la API correctamente', async () => {
    const vm = createVm();
    await vm.guardar();
    expect(vm.$http.post).toHaveBeenCalledWith(
      '/mi-endpoint',
      expect.objectContaining({ campo: expect.any(String) })
    );
  });
});
```

Ejecutar solo el nuevo test:

```bash
npx jest --config jest.config.cjs tests/jest/components/MiComponente.spec.js
```

### Nuevo test PHPUnit Feature

Crear o agregar tests en `tests/Feature/`. Los tests de QA usan **Guzzle directamente** (no `$this->get()` de Laravel) porque corren contra un servidor real:

```php
<?php

namespace Tests\Feature;

use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;

class MiNuevoTest extends TestCase
{
    private static string $baseUrl = '';
    private static ?Client $client = null;
    private static ?string $apiToken = null;

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();
        self::loadQaEnv();
        self::applyEnvAliases();

        self::$baseUrl = rtrim((string) (getenv('BASE_URL') ?: ''), '/');
        self::$apiToken = getenv('API_TOKEN') ?: null;

        if (self::$baseUrl !== '') {
            self::$client = new Client([
                'base_uri' => self::$baseUrl,
                'timeout'  => 30,
                'http_errors' => false,
                'verify' => false,
            ]);
        }
    }

    public function testMiEndpoint(): void
    {
        $response = $this->requireClient()->get('/api/mi-endpoint', [
            'headers' => [
                'Authorization' => 'Bearer ' . self::$apiToken,
                'Accept' => 'application/json',
            ],
        ]);

        $this->assertContains($response->getStatusCode(), [200, 201]);
        $data = json_decode($response->getBody()->getContents(), true);
        $this->assertArrayHasKey('data', $data);
    }

    // --- Helpers (copiar de EndpointRegressionTest) ---

    private function requireClient(): Client
    {
        if (self::$client === null) {
            $this->markTestSkipped('BASE_URL no configurada');
        }
        return self::$client;
    }

    private static function loadQaEnv(): void
    {
        $files = ['.env.qa.local', '.env.qa', '.env'];
        foreach ($files as $file) {
            $path = __DIR__ . '/../../' . $file;
            if (!file_exists($path)) continue;
            foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
                $line = trim($line);
                if ($line === '' || $line[0] === '#') continue;
                putenv($line);
            }
        }
    }

    private static function applyEnvAliases(): void
    {
        $map = [
            'QA_BASE_URL' => 'BASE_URL',
            'QA_API_TOKEN' => 'API_TOKEN',
            'QA_API_EMAIL' => 'API_EMAIL',
            'QA_API_PASSWORD' => 'API_PASSWORD',
        ];
        foreach ($map as $from => $to) {
            if (!getenv($to) && getenv($from)) {
                putenv("$to=" . getenv($from));
            }
        }
    }
}
```

Ejecutar:

```bash
php vendor/bin/phpunit --filter MiNuevoTest
```

---

## Pipeline CI/CD (GitHub Actions)

**Archivo:** `.github/workflows/qa.yml`
**Trigger:** Push y PR a cualquier branch, mas `workflow_dispatch` manual.

### Jobs

Todos los jobs de prueba dependen de `encoding_guard` (que corre primero). Luego se ejecutan en paralelo:

```
encoding_guard (5 min)
    ├── api_regression     (30 min timeout)
    ├── e2e_tests          (45 min timeout)
    ├── jest_unit          (20 min timeout)
    ├── static_analysis    (30 min timeout)
    └── backend_feature_qa (35 min timeout)
```

| Job | Runner | Herramientas | Secretos requeridos |
|-----|--------|-------------|---------------------|
| `encoding_guard` | ubuntu + Node 20 | npm ci | Ninguno |
| `api_regression` | ubuntu + Node 20 | npm ci | `QA_BASE_URL`, `QA_API_TOKEN` o email/pass, `QA_WEB_*` |
| `e2e_tests` | ubuntu + Node 20 + Chromium | npm ci + playwright install | `QA_BASE_URL`, `QA_WEB_*` |
| `jest_unit` | ubuntu + Node 20 | npm ci | Ninguno |
| `static_analysis` | ubuntu + PHP 8.3 | composer install | Ninguno |
| `backend_feature_qa` | ubuntu + PHP 8.3 | composer install | `QA_BASE_URL`, `QA_API_TOKEN` o email/pass, `QA_WEB_*` |

### Artefactos generados

| Job | Artefacto | Contenido |
|-----|----------|-----------|
| `api_regression` | `api-regression-report` | `storage/logs/api_regression_*.json` |
| `e2e_tests` | `playwright-results` | `.runtime/playwright/test-results/` (traces, screenshots, videos) |
| `jest_unit` | `jest-coverage` | `coverage/` (reporte lcov + text-summary) |

### Validacion de secretos

Los jobs que requieren acceso al tenant (`api_regression`, `e2e_tests`, `backend_feature_qa`) validan los secretos al inicio. Si no estan configurados, el job falla rapido con un mensaje descriptivo.

---

## Ejecucion local (Windows + Laragon)

### Requisitos previos

1. **Node.js 20+** instalado y en PATH
2. **PHP 8.3** disponible (via Laragon)
3. `npm ci` ejecutado (instala dependencias)
4. `composer install` ejecutado (para PHPUnit y PHPStan)
5. `.env.qa` configurado con credenciales del tenant de pruebas

### Ejecutar todo

```bash
# Suite rapida (API + E2E + Jest, ~5-10 min)
npm run qa:all

# Suite completa (incluye PHPUnit, ~10-15 min)
npm run qa:full
```

### PHP CLI en Windows

El PHP del sistema puede apuntar a una version distinta. Para PHPUnit y PHPStan, usar el binario de Laragon:

```powershell
# PHPUnit
c:/laragon/bin/php/php-8.3.27-Win32-vs16-x64/php.exe vendor/bin/phpunit --testsuite Feature

# PHPStan
c:/laragon/bin/php/php-8.3.27-Win32-vs16-x64/php.exe vendor/bin/phpstan analyse --memory-limit=1G
```

### Instalar navegadores Playwright (primera vez)

```bash
npx playwright install chromium
```

---

## Troubleshooting

### Error de autenticacion API

**Sintoma:** Todos los tests API fallan con 401/403.

**Solucion:** Verificar que `QA_API_TOKEN` es valido para el tenant, o que `QA_API_EMAIL`/`QA_API_PASSWORD` son correctos:

```bash
# Probar manualmente
curl -H "Authorization: Bearer TU_TOKEN" http://demo.enterfarmaplus.test/api/company
```

### Error de login web (E2E)

**Sintoma:** Tests E2E fallan con `Login invalido. HTTP 422` o `HTTP 419`.

**Solucion:**
- Verificar `QA_WEB_EMAIL` y `QA_WEB_PASSWORD` en `.env.qa`
- Un 419 indica problema de CSRF. Asegurar que el servidor genera tokens correctamente
- Verificar que el tenant esta activo y accesible

### Timeout en requests

**Sintoma:** Tests fallan con `timeout of Xms exceeded`.

**Solucion:** Aumentar el timeout en `.env.qa`:

```
REQUEST_TIMEOUT_MS=60000
```

Para E2E, el timeout global se configura en `playwright.config.js` (default: 120s).

### PHPStan falla con errores nuevos

**Sintoma:** PHPStan reporta errores que no existian antes.

**Solucion:** Los errores preexistentes estan en `phpstan-baseline.neon`. Solo errores nuevos rompen el build.

Para regenerar la baseline (agregar tus errores al baseline):

```bash
php vendor/bin/phpstan analyse --memory-limit=1G --generate-baseline
```

**Nota:** Solo regenerar baseline cuando los errores son aceptables y no se van a corregir de inmediato.

### Jest falla por dependencias no mockeadas

**Sintoma:** `Cannot find module 'X'` o `ReferenceError: X is not defined`.

**Solucion:** Agregar un mock en `tests/jest/__mocks__/`. Ejemplo para una dependencia externa:

```javascript
// tests/jest/__mocks__/miDependencia.js
module.exports = {};
```

Y registrar en `jest.config.cjs`:

```javascript
moduleNameMapper: {
  '^mi-dependencia$': '<rootDir>/tests/jest/__mocks__/miDependencia.js',
}
```

### Playwright no encuentra navegador

**Sintoma:** `browserType.launch: Executable doesn't exist`.

**Solucion:**

```bash
npx playwright install chromium
# o con dependencias del sistema (Linux CI)
npx playwright install --with-deps chromium
```

### Tests E2E muestran errores "de ruido"

**Sintoma:** Tests pasan pero muestran errores de debugbar, favicon 404, PhpDebugBar undefined.

**Explicacion:** En modo normal (`qa:e2e`), estos errores se filtran automaticamente. En modo estricto (`qa:e2e:strict`), se reportan todos. Esto es intencional para poder detectar regresiones intermitentes cuando se necesite.

### CI falla en validacion de secretos

**Sintoma:** Job falla con `Missing QA_BASE_URL secret`.

**Solucion:** Configurar los secrets en GitHub: Settings > Secrets and variables > Actions. Los secrets necesarios son `QA_BASE_URL`, `QA_API_TOKEN`, `QA_WEB_EMAIL`, `QA_WEB_PASSWORD`.
