# e2e-panel — гайд для встраивания (для LLM)

> Этот документ — машинно-читаемый контракт. Если ты LLM, которой поручили
> встроить e2e-panel в проект, читай дальше. Здесь всё, что нужно: пути,
> схемы, API, CSP, примеры.

## Что это

e2e-panel — плавающая панель ручного e2e-тестирования. Виджет живёт на
отдельном домене (`e2e-panel.aipika.tech`) как статика (Vue 3 SFC через CDN,
Shadow DOM). На тестируемый сайт он подключается кросс-доменно — **не входит
в сборку проекта**. Сценарии и действия виджет берёт с домена-носителя из
файла `/.e2e/tests.yaml`.

## Архитектура

```
тестируемый сайт (example.com)          домен виджета (e2e-panel.aipika.tech)
┌──────────────────────────┐           ┌──────────────────────┐
│  HTML-страница            │           │  loader.js           │ ← статика
│  <script src="…/loader.js">──fetch────│  widget.vue          │
│                           │           │  (nginx, CORS: *)   │
│  /.e2e/tests.yaml ◄───────│──fetch────│                      │
│  /api/e2e/feedback ◄──────│──POST─────│                      │
│  /api/e2e/reset   ◄───────│──POST─────│                      │
└──────────────────────────┘           └──────────────────────┘
```

Виджет (JS в Shadow DOM) делает два fetch'а на тестируемый домен:
1. `GET /.e2e/tests.yaml` — сценарии + действия (при загрузке)
2. `POST /api/e2e/feedback` — очередь комментариев/правок (по кнопкам)

Действия из YAML дёргают произвольные API-эндпоинты тестируемого домена
(логин, сброс, сидинг и т.д.) — это host-специфичные роуты.

## Быстрый старт (3 шага)

1. **Добавь файл сценариев** на тестируемый домен по пути `/.e2e/tests.yaml`.
   Файл — обычный текстовый YAML, отдаётся как статика без авторизации.

2. **Подключи виджет** на страницах (или используй Chrome-расширение):
   ```html
   <script async src="https://e2e-panel.aipika.tech/loader.js"></script>
   ```

3. **(Опционально) Реализуй `/api/e2e/feedback`** для очереди комментариев и
   правок сценариев в Claude Code. Без него виджет работает, но фидбек
   сохраняется локально (скриншот скачивается, текст копируется в буфер).

Готово. Панель появится в правом нижнем углу.

## Что нужно сделать на стороне тестируемого проекта

### 1. Файл `/.e2e/tests.yaml`

Файл **обязан** быть доступен по пути `/.e2e/tests.yaml` на том же origin,
где открыта страница с виджетом. Без авторизации, без редиректов.

**Важно для фреймворков с middleware (Next.js, Nuxt, etc.):** исключи путь
`/.e2e/` из middleware-матчера, иначе i18n/auth/redirect-middleware уведёт
запрос на `/ru/.e2e/tests.yaml` или на страницу логина → 404.

Пример для Next.js (файл `middleware.ts` или `proxy.ts`):
```ts
export const config = {
  // `\\.e2e` — исключает /.e2e/* из middleware
  matcher: ['/', '/(ru|en)/:path*', '/((?!api|_next/static|\\.e2e|.*\\.png$).*)'],
};
```

Пример для Express:
```js
app.get('/.e2e/tests.yaml', (req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  res.sendFile(path.join(__dirname, 'public/.e2e/tests.yaml'));
});
```

Пример для nginx (если статика отдаётся nginx'ом):
```nginx
location /.e2e/ {
    alias /var/www/.e2e/;
    add_header Cache-Control "no-store";
    add_header Access-Control-Allow-Origin "*";
}
```

### 2. CSP (Content-Security-Policy)

Если проект использует CSP-заголовки, добавь домен виджета в `script-src` и
`connect-src`:

```
script-src  'self' https://e2e-panel.aipika.tech https://unpkg.com https://cdn.jsdelivr.net;
connect-src 'self' https://e2e-panel.aipika.tech;
```

Виджет грузит с CDN: `unpkg.com` (Vue 3), `cdn.jsdelivr.net` (vue3-sfc-loader,
js-yaml) и `fonts.googleapis.com` (Inter Tight). Если CSP строгий — добавь все
четыре домена.

### 3. (Опционально) API-эндпоинты

Виджет вызывает host-эндпоинты, которые описаны в YAML (`actions` и
`feedback`). Реализуй те, что нужны. Контракты — ниже.

## Схема YAML: `/.e2e/tests.yaml`

```yaml
version: 1                 #整数, всегда 1

# Глобальные действия — доступны во всех сценариях (селект «общие»).
# Необязательно. Каждое действие — кнопка в селекте «Действия».
actions:
  - label: <string>        # текст в селекте (обязательно)
    url: <string>          # API-эндпоинт (относительный или абсолютный)
    method: <string>       # GET | POST | PUT | DELETE, умолч. POST
    body: <object|null>    # тело запроса (→ JSON), только для не-GET
    clear: <string|bool>   # all | storage | cookies | true(=all), до fetch
    confirm: <bool>        # показать confirm() перед выполнением
    reload: <bool>         # перезагрузить страницу после успеха
    redirect: <string>     # перейти на URL после успеха (вместо reload)

# Настройка очереди фидбека (комментарии + правки сценариев).
# Необязательно. По умолчанию { url: /api/e2e/feedback, method: POST }.
# Если url пустой — фидбек сохраняется локально (фолбэк без бэкенда).
feedback:
  url: <string>            # умолч. /api/e2e/feedback
  method: <string>         # умолч. POST

# Иерархия: группы → сценарии. groups[] — с заголовками (optgroup).
# Плоский scenarios[] без groups — одна безымянная группа.
groups:
  - id: <string>           # короткий идентификатор группы (буква/число)
    title: <string>        # заголовок группы в селекте
    scenarios:
      - index: <string>    # индекс сценария → «A1 · …» в селекте
        title: <string>    # название сценария
        steps:             # список строк (простой текст, ничего не парсится)
          - <string>
          - <string>
        # Действия, специфичные для этого сценария (селект «этот сценарий»).
        # Необязательно. Формат полей — как в глобальных actions.
        actions:
          - label: <string>
            url: <string>
            # ... те же поля, что в глобальных actions

# Альтернатива groups — плоский список (без заголовков):
# scenarios:
#   - index: S1
#     title: ...
#     steps: [...]
```

### Правила

- `steps` — массив строк. Смену роли обозначаем строкой `ты - клиент` /
  `ты - админ` (текст, не специальный синтаксис).
- `id` сценария = `index || id || title` (первое непустое). По нему
  восстанавливается выбор из localStorage.
- Действия могут быть глобальными (`actions` в корне) и/или на сценарии
  (`actions` внутри сценария). В селекте они группируются: «этот сценарий» →
  «общие» → «виджет».
- `clear` выполняется **до** fetch. `reload` / `redirect` — **после** успеха.
- Относительный `url` бьёт по `location.origin` с `credentials: same-origin`
  (куки сессии уходят автоматически).

## Контракт: `POST /api/e2e/feedback`

Виджет шлёт JSON при нажатии кнопок «комментарий» (у шага) и «✎» (правка
сценария). Хост должен сохранить заявку для последующего разбора (файлы на
диск, БД — на выбор).

### Запрос

```jsonc
{
  // Общие поля (всегда):
  "type": "comment",              // "comment" | "scenario-edit"
  "pageUrl": "https://example.com/dashboard",
  "userAgent": "Mozilla/5.0 …",

  // Поля сценария (всегда):
  "scenarioId": "A1",
  "scenarioIndex": "A1",
  "scenarioTitle": "Регистрация нового клиента",

  // Для type=comment:
  "comment": "текст комментария",
  "screenshot": "data:image/png;base64,iVBOR…",  // dataURL PNG, может отсутствовать
  "stepNumber": 3,               // номер шага (1-based)
  "stepText": "Создай объявление",

  // Для type=scenario-edit:
  "before": "ты - клиент\nОткрой страницу…",   // текущие шаги (до правки)
  "proposed": "ты - клиент\nОткрой главную…"   // предложенные шаги (после правки)
}
```

### Ответ

```json
{ "ok": true, "id": "<uuid>", "type": "comment" }
```
HTTP 201. Виджет показывает «✓ комментарий в очереди». При ошибке (4xx/5xx)
показывает «✕ очередь — <status>».

### Рекомендуемая реализация (запись на диск)

Эндпоинт пишет пары файлов в `storage/e2e-queue/`:

```
<ISO-дата>__comment__<uuid8>.json     # метаданные заявки
<ISO-дата>__comment__<uuid8>.png      # скриншот (если есть)
<ISO-дата>__scenario-edit__<uuid8>.json
```

Лимиты (рекомендуемые): скриншот ≤ 8 МБ, текст ≤ 20 000 символов.

### Разбор очереди из Claude Code / Codex

```bash
ls storage/e2e-queue/         # что накопилось
cat storage/e2e-queue/*.json  # прочитать заявки
# скриншоты — рядом,同名 .png
```

## Контракт: `POST /api/e2e/reset` (опциональный)

Вызывается действием `clear: all` + `url: /api/e2e/reset`. Назначение —
полный сброс сессий на бэкенде (httpOnly-куки нельзя удалить клиентски).

```json
// Ответ:
{ "ok": true }
```

Хост-специфичная логика: обычно гасит все httpOnly-сессионные куки (клиент,
админ и т.д.). Виджету важен только статус-код: 2xx = успех, reload после.

## Контракт: прочие action-эндпоинты

Любой URL из `actions` вызывается как:
```
fetch(url, { method, credentials: 'same-origin', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) })
```
Виджету важен только HTTP-статус. Тело ответа не используется. Хост
реализует произвольные эндпоинты (demo-login, seed-demo, delete-user и т.д.)
под нужды проекта.

## Способы встраивания

| Способ | Где | Что делает |
|--------|-----|------------|
| `<script>` тег | HTML страницы | Авто-загрузка на каждой странице |
| Букмарклет | закладка браузера | Инжект по клику на любой странице |
| Chrome-расширение | `extension/` | Кнопка в попапе + авто-запуск по доменам |

### 1. Script-тег (в коде страницы)

```html
<script async src="https://e2e-panel.aipika.tech/loader.js"></script>
```

### 2. Букмарклет

```
javascript:(function(){var s=document.createElement('script');s.async=true;s.src='https://e2e-panel.aipika.tech/loader.js';document.body.appendChild(s);})()
```

### 3. Chrome-расширение

Папка `extension/` → загрузить как unpacked extension в `chrome://extensions`.
Клик по иконке → «Запустить панель». Можно включить авто-запуск на домене.

## JavaScript-оверрайды

Задать **до** подключения `loader.js`:

```js
window.__E2E_PANEL_BASE__       // string: базовый URL для widget.vue
                                // умолч. = домен, откуда загружен loader.js
window.__E2E_PANEL_TESTS_URL__  // string: URL для tests.yaml
                                // умолч. /.e2e/tests.yaml (относительно origin страницы)
```

Идемпотентность: повторная загрузка `loader.js` игнорируется (проверка
`window.__E2E_PANEL__`).

## Локальная отладка

```bash
node serve.mjs    # http://localhost:3070/ — статика виджета с CORS
```

Подключить виджет к локальному приложению (например, на :3060) — в консоли
вкладки приложения:

```js
window.__E2E_PANEL_BASE__ = 'http://localhost:3070/';
var s = document.createElement('script');
s.async = true; s.src = 'http://localhost:3070/loader.js';
document.body.appendChild(s);
```

Виджет грузит `widget.vue` с :3070, а сценарии — с origin страницы (:3060)
по `/.e2e/tests.yaml`.

## Деплой домена виджета

```bash
node ../rsync-docker-compose/index.js root@<хост>
```

`docker-compose.yaml` → nginx:alpine + статика + CORS + traefik для
`e2e-panel.aipika.tech`. Let's Encrypt — автоматически.

## Чек-лист встраивания

```
□  /.e2e/tests.yaml отдаётся на тестируемом домене (200, no-store)
□  /.e2e/ исключён из middleware (i18n, auth, redirects)
□  CSP пропускает e2e-panel.aipika.tech + CDN домены (если есть CSP)
□  <script async src="…/loader.js"> добавлен на страницы (или extension)
□  /api/e2e/feedback реализован (опционально, но рекомендуется)
□  Action-эндпоинты из YAML реализованы на бэкенде
□  storage/e2e-queue/ в .gitignore (если feedback пишет на диск)
```
