# Functional Test Script Guide

> This document defines the conventions for generated functional test scripts.
> Used by `/tas-functest-mobile` and `/tas-functest-web` commands.
> Kit v3 — anchored on Feature (no Epic / Story).

---

## File Structure Convention

### Mobile (Detox)
```
apps/mobile/e2e/
├── features/                           # Layer 2: Functional test scripts
│   ├── {feature-slug}/
│   │   ├── {feature-slug}.func.e2e.ts  # One file per Feature
│   │   ├── helpers.ts                  # Feature-specific helpers (reusable by E2E)
│   │   └── index.ts                    # Barrel export
│   └── ...
├── flows/                              # Layer 3: E2E scripts (separate)
├── data/                               # Test data per environment
│   ├── test-data.dev.json
│   ├── test-data.staging.json
│   └── test-data.prod.json
├── helpers/                            # Shared helpers
│   ├── data-loader.ts
│   └── test-utils.ts
└── test-ids.ts                         # Centralized test ID registry
```

### Web (Playwright)
```
apps/web/e2e/
├── features/                           # Layer 2: Functional test scripts
│   ├── {feature-slug}/
│   │   ├── {feature-slug}.func.spec.ts # One file per Feature
│   │   ├── helpers.ts
│   │   └── index.ts
│   └── ...
├── flows/                              # Layer 3: E2E scripts
├── data/
│   ├── test-data.dev.json
│   ├── test-data.staging.json
│   └── test-data.prod.json
├── helpers/
│   ├── data-loader.ts
│   └── test-utils.ts
└── page-objects/                       # Playwright page objects
```

---

## File Naming Convention

| Layer | Platform | Suffix | Example |
|-------|----------|--------|---------|
| Functional | Mobile | `.func.e2e.ts` | `login-form.func.e2e.ts` |
| Functional | Web | `.func.spec.ts` | `login-form.func.spec.ts` |
| E2E Flow | Mobile | `.e2e.ts` | `auth-complete-flow.e2e.ts` |
| E2E Flow | Web | `.spec.ts` | `auth-complete-flow.spec.ts` |

---

## Script Template: Mobile (Detox)

```typescript
/**
 * Functional Tests: {Feature Name}
 * Feature: {Feature_ID}
 * Stack: app
 *
 * Generated by /tas-functest-mobile
 * Spec: docs/features/{feature-dir}/{CODE}-FuncTest.md
 */

import { device, element, expect, by, waitFor } from 'detox';
import { TEST_IDS } from '../../test-ids';
import { loadTestData, getCredentials } from '../../helpers/data-loader';
import { login, navigateTo } from '../../helpers/test-utils';

const testData = loadTestData();

describe('{Feature Name}', () => {
  beforeAll(async () => {
    await device.launchApp({ newInstance: true });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  // AC Reference: AC-1 — title = FuncTest §4 TC ID verbatim (F{NNN}_AC{N}_{CAT}_{SEQ})
  describe('F001_AC1_BL_001', () => {
    it('should {happy path description}', async () => {
      // Given: {precondition}
      // When: {action}
      await element(by.id(TEST_IDS.FEATURE.ELEMENT)).tap();
      // Then: {expected}
      await expect(element(by.id(TEST_IDS.FEATURE.RESULT))).toBeVisible();
    });
  });

  // AC Reference: AC-1
  describe('F001_AC1_VAL_001', () => {
    it('should {negative scenario description}', async () => {
      await element(by.id(TEST_IDS.FEATURE.INPUT)).typeText('invalid');
      await element(by.id(TEST_IDS.FEATURE.SUBMIT)).tap();
      await expect(element(by.id(TEST_IDS.FEATURE.ERROR))).toBeVisible();
    });
  });
});
```

---

## Script Template: Web (Playwright)

```typescript
/**
 * Functional Tests: {Feature Name}
 * Feature: {Feature_ID}
 * Stack: web
 *
 * Generated by /tas-functest-web
 * Spec: docs/features/{feature-dir}/{CODE}-FuncTest.md
 */

import { test, expect } from '@playwright/test';
import { loadTestData, getCredentials } from '../../helpers/data-loader';

const testData = loadTestData();

test.describe('{Feature Name}', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/target-page');
  });

  // AC Reference: AC-1 — title = FuncTest §4 TC ID verbatim (F{NNN}_AC{N}_{CAT}_{SEQ})
  test.describe('F001_AC1_BL_001', () => {
    test('should {happy path description}', async ({ page }) => {
      await page.getByTestId('feature-element').click();
      await expect(page.getByTestId('feature-result')).toBeVisible();
    });

    // Cross-viewport — SAME TC ID, viewport as bracketed variant (parseable, not a new ID)
    for (const viewport of [
      { width: 375, height: 812, name: 'mobile' },
      { width: 768, height: 1024, name: 'tablet' },
      { width: 1280, height: 720, name: 'desktop' },
    ]) {
      test(`F001_AC1_BL_001 [${viewport.name}]`, async ({ page }) => {
        await page.setViewportSize({ width: viewport.width, height: viewport.height });
      });
    }
  });

  // AC Reference: AC-1
  test.describe('F001_AC1_VAL_001', () => {
    test('should {negative scenario description}', async ({ page }) => {
      await page.getByTestId('feature-input').fill('invalid');
      await page.getByTestId('feature-submit').click();
      await expect(page.getByTestId('feature-error')).toBeVisible();
    });
  });
});
```

---

## Data Loading Pattern

```typescript
import { loadTestData, getCredentials } from '../../helpers/data-loader';

const testData = loadTestData();
const creds = getCredentials();
```

---

## describe/it Block Naming Convention

The inner `describe` title MUST be the FuncTest §4 TC ID **verbatim** — `F{NNN}_AC{N}_{CAT}_{SEQ}`, CAT ∈ `GUI|VAL|BL|EXC|SEC|INT|PERF`. Never synthesize a new ID (no `_FT_`, no `{PROJECT}` prefix, no `_H/_E/_N` modifier); `/tas-functest-report` parses the title back with `^(F\d+_AC\d+_[A-Z]+_\d+[a-z]?)` (trailing letter = BVA/EP variant, kept as a distinct TC ID).

```typescript
describe('{Feature Name}', () => {
  describe('F001_AC1_BL_001', () => {
    it('should {action} when {condition}', async () => { ... });
  });
});
```

---

## Reusable Helpers

Feature-specific helpers should be exported for Layer 3 (E2E) reuse:

```typescript
// features/{feature-slug}/helpers.ts
export async function fillLoginForm(email: string, password: string) {
  await element(by.id(TEST_IDS.AUTH.LOGIN.EMAIL_INPUT)).typeText(email);
  await element(by.id(TEST_IDS.AUTH.LOGIN.PASSWORD_INPUT)).typeText(password);
  await element(by.id(TEST_IDS.AUTH.LOGIN.SUBMIT_BUTTON)).tap();
}

export async function verifyLoginSuccess() {
  await waitFor(element(by.id(TEST_IDS.HOME.SCREEN)))
    .toBeVisible()
    .withTimeout(5000);
}
```

These helpers are imported by E2E flow scripts in `flows/` to avoid duplication.

---

## package.json Script Convention

```json
{
  "scripts": {
    "functest:mobile:{feature-slug}": "detox test --configuration ios.sim.debug --testPathPattern='features/{feature-slug}'",
    "functest:web:{feature-slug}": "npx playwright test e2e/features/{feature-slug}",
    "functest:mobile:all": "detox test --configuration ios.sim.debug --testPathPattern='features/'",
    "functest:web:all": "npx playwright test e2e/features/"
  }
}
```
