Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/mobile-smoke-polish/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!-- @format -->

# Проверка исправлений после мобильного smoke-test

Эталон desktop: `origin/dev` на `706a574ed916c6b0a78e99402588ee4889657e74`, после восстановления desktop в #403 и форматирования в #404. Эта проверка подтверждает отсутствие новых desktop-регрессий относительно уже восстановленного dev; сравнение с состоянием до #401 находится в `docs/desktop-regression`.

## Исправления и acceptance criteria

| Замечание | Результат | Evidence на 390 px |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 1. Приглашение смещено влево | Карточка центрируется внутри списка на mobile/tablet | [Приглашение](screenshots/projects-invites-390.png) |
| 2. Логотип, подсказка и верхние кнопки пересекаются | Логотип центрируется, подсказка вынесена за его границы; действия расположены вертикально с отступами | [Редактор](screenshots/project-edit-390.png) |
| 3. Узкая дата, перекрытие подсказкой, разные шрифты | Дата занимает ширину колонки; под подсказку зарезервировано место; размеры текста региона и остальных полей совпадают | [Поля](screenshots/project-edit-fields-390.png) |
| 4. Кнопка добавления цели смещена | Кнопки занимают ширину формы, текст переносится; новая цель тоже имеет вертикальную раскладку полей | [Новая цель](screenshots/project-edit-new-goal-390.png) |
| 5. Партнёр и ресурс без отступа | Между кнопками 16 px | [Партнёры](screenshots/partners-390.png) |
| 6. Поиск участника выглядит как вложенное поле | Внешний контейнер владеет рамкой и focus-состоянием; внутренний input не имеет своей рамки | [Приглашение участника](screenshots/team-invite-390.png) |
| 7. Кнопки модального окна смещены вправо | Кнопки занимают всю ширину footer с вертикальным отступом | [Приглашение участника](screenshots/team-invite-390.png) |
| 8. Бесполезные «Контакты» программы | Пустая кнопка скрыта; при наличии ссылок мобильная кнопка открывает контакты, доступные на desktop в боковой колонке | [Программа без контактов](screenshots/program-390.png) |
| 9. «Требует внимания» выглядит как полосы прогресса | Снята фиолетовая заливка shared Button; строки сохраняют числа, переходы и подсказки | [Аналитика](screenshots/analytics-attention-390.png) |
| 10. Дублирующиеся ошибки соединения | Одно закрываемое сообщение на период недоступности чата; автоматическое переподключение и снятие сообщения после восстановления | [Уведомление](screenshots/unavailable-390.png), `connection.cjs` |

Все изменения существующей геометрии и типографики ограничены `<1000px`. Стиль новой кнопки закрытия уведомления применяется только к этой кнопке. Mont, палитра и desktop-композиция сохранены.

## Почему было два сообщения о соединении

`ConnectionStatusToastService` и `ChatRealtimeRepository` подписывались на один `WebsocketService.connectionLost$` и создавали два разных snackbar. После порога повторных попыток событие приходило снова, примерно каждые пять секунд, поэтому уведомления постоянно обновлялись.

Теперь UX принадлежит одному сервису. Статус WebSocket различает подключение, недоступность и завершённую сессию. При длительном сбое появляется текст: «Чат временно недоступен. Сообщения и статусы могут обновляться с задержкой. Подключаемся автоматически». Это не утверждение о недоступности HTTP API или интернета в целом.

Уведомление можно закрыть. Закрытое сообщение не появляется снова в течение того же сбоя. Если его не закрывать, оно исчезает при восстановлении WebSocket; следующий отдельный сбой создаёт новое сообщение. Повторные подключения продолжаются автоматически. Явное закрытие сессии отменяет ожидающий retry, а teardown освобождает обработчики и старый socket.

API, авторизация, правила доступа, payload сообщений, очередь исходящих сообщений и бизнес-процессы проектов/программ/приглашений не менялись. Изменены транспортный lifecycle и отображение его состояния.

## Выполненные проверки

- `npm run test:vitest:ci`: **2016 тестов, 407 файлов, exit 0**. В существующем `vitest.config.ts` по-прежнему исключены три legacy spec: `projects.component.spec.ts`, `program.component.spec.ts`, `editor-submit-button.directive.spec.ts`; они не представлены как проверенные.
- `npm run build:social:prod`: exit 0. Существующие предупреждения сборки сохранены.
- `npm run lint:ts`: exit 0, 0 ошибок и 6 существующих предупреждений вне этого diff.
- Stylelint изменённых SCSS: exit 0.
- `npm run format:check`: проверяется на Git index snapshot с LF, без приватных build/screenshot-каталогов; команда и конфигурация совпадают с CI.
- Браузер: 16 основных экранов на **320, 390, 768, 1440, 1920 px**, плюс 6 дополнительных состояний после прокрутки/выбора участника на каждой ширине. Проверены центрирование, расстояния между кнопками, ширина даты, отсутствие перекрытия подсказками, одинаковый размер шрифта, рамка поиска, enabled/disabled приглашение и отсутствие горизонтального overflow/console errors.
- Desktop pixel regression: **44/44 точных совпадений** на 1440/1920 px, **0 изменённых RGB-пикселей**, одинаковые размеры изображений. Нет масок и допуска. Данные: [desktop-comparison.json](desktop-comparison.json).
- `connection.cjs`: синтетический сбой WebSocket при доступном HTTP; одно уведомление, ручное закрытие, отсутствие повторного появления в том же сбое, восстановление, новый отдельный сбой и автоматическое скрытие — passed.
- `visual.cjs contacts`: непустая кнопка контактов на 390 px открывает модальное окно с существующей ссылкой.

Основные экраны: Feed, проекты, участники, программы, программа, профиль, проект, вакансии, вакансия, курсы, курс, редактор профиля, основные данные проекта, партнёры/ресурсы, аналитика программы, приглашение участника. Дополнительные состояния: список приглашений, дата/поля проекта, кнопки целей, новая цель, строки внимания, выбранный участник.

При загрузке через локальный HTTP/proxy Chromium несколько раз прервал загрузку JS с `net::ERR_NO_BUFFER_SPACE` до bootstrap приложения. Финальные visual-проверки выполнены с `QA_BUILD_ROOT`: `preview.cjs` подаёт браузеру неизменённые файлы соответствующей сборки напрямую через Playwright route. Это устраняет сбой локальной доставки JS, не подменяя DOM, CSS или результат рендеринга. Все страницы проверяются заново; проваленные инфраструктурные запуски не считаются evidence.

## Воспроизведение visual regression

Нужны Chrome/Chromium, Playwright, Python с Pillow и NumPy. `PLAYWRIGHT_MODULE` и `CHROME_PATH` позволяют указать существующие runtime без изменения зависимостей проекта. Все API и WebSocket в harness заменены синтетическими fixtures; запросы на live API не отправляются.

Для воспроизводимой проверки установить `QA_BUILD_ROOT` в путь к нужной сборке: `before-dist` для обоих запусков `before`, `after-dist` для `after`, `connection` и `contacts`. Например, PowerShell: `$env:QA_BUILD_ROOT = '.desktop-regression/mobile-smoke/after-dist'`. Без этой переменной скрипты используют обычный локальный HTTP preview.

1. Собрать baseline из указанного выше commit в отдельном checkout и исправленную ветку одинаковой development-конфигурацией. Сохранить результаты как `before-dist` и `after-dist`.
2. В отдельных терминалах запустить preview:

```text
node docs/desktop-regression/serve.cjs .desktop-regression/mobile-smoke/before-dist 4451
node docs/desktop-regression/serve.cjs .desktop-regression/mobile-smoke/after-dist 4452
```

3. Снять основные и дополнительные состояния для обеих сборок:

```text
node docs/mobile-smoke-polish/visual.cjs before http://127.0.0.1:4451 320,390,768,1440,1920 .desktop-regression/mobile-smoke/screenshots
node docs/mobile-smoke-polish/visual.cjs after http://127.0.0.1:4452 320,390,768,1440,1920 .desktop-regression/mobile-smoke/screenshots
node docs/mobile-smoke-polish/visual.cjs before http://127.0.0.1:4451 320,390,768,1440,1920 .desktop-regression/mobile-smoke/screenshots details
node docs/mobile-smoke-polish/visual.cjs after http://127.0.0.1:4452 320,390,768,1440,1920 .desktop-regression/mobile-smoke/screenshots details
python docs/mobile-smoke-polish/compare.py .desktop-regression/mobile-smoke/screenshots/before .desktop-regression/mobile-smoke/screenshots/after docs/mobile-smoke-polish/desktop-comparison.json
node docs/mobile-smoke-polish/connection.cjs http://127.0.0.1:4452
node docs/mobile-smoke-polish/visual.cjs contacts http://127.0.0.1:4452 390
```

В PowerShell аргумент со списком ширин следует заключить в кавычки. Ширины можно запускать по отдельности в тот же каталог: PNG имеют уникальные имена, comparator проверяет все 44 обязательные desktop-пары и падает при отсутствии любой из них. Harness сбрасывает прокрутку окна и внутренних scroll-контейнеров перед основным скриншотом; дополнительные состояния прокручиваются явно. После генерации JSON отформатировать Prettier перед commit. Полные build/PNG-артефакты остаются в приватном `.desktop-regression`, в Git включены representative screenshots и результаты сравнения.

## Review и ограничения

Source review: desktop media boundary, отсутствие изменений API/permission/domain-правил, сохранность очереди сообщений, отмена retry при закрытии, lifecycle подписок, доступность close-кнопки и сохранение focus на контейнере поиска — проверены. Существенных незакрытых замечаний в этом diff нет.

Браузерные проверки выполнены в headless Chrome на Windows с fixtures. **Реальные iOS/Android, Safari/WebKit, загрузка файлов, live API и реальный сетевой обрыв на dev-стенде не проверены.** После merge требуется повторный smoke-test на устройствах по десяти пунктам выше, включая уход приложения в фон и восстановление чата. Изменения данных и backend migration не требуются.
43 changes: 43 additions & 0 deletions docs/mobile-smoke-polish/compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Strict RGB desktop regression gate, including scrolled and modal states."""
import argparse
import json
from pathlib import Path

import numpy as np
from PIL import Image

parser = argparse.ArgumentParser()
parser.add_argument("before", type=Path)
parser.add_argument("after", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
rows = []
screens = (
"feed projects members programs program profile project vacancies vacancy courses course "
"profile-edit project-edit partners analytics team-invite projects-invites "
"project-edit-fields project-edit-goals project-edit-new-goal analytics-attention "
"team-invite-selected"
).split()
files = sorted(args.before / f"{screen}-{width}.png" for screen in screens for width in (1440, 1920))
for file in files:
before = Image.open(file).convert("RGB")
after = Image.open(args.after / file.name).convert("RGB")
same_size = before.size == after.size
if same_size:
delta = np.max(np.abs(np.asarray(before).astype(np.int16) - np.asarray(after).astype(np.int16)), axis=2)
changed = int(np.count_nonzero(delta))
maximum = int(delta.max())
else:
changed = -1
maximum = -1
rows.append({"screen": file.stem, "beforeSize": before.size, "afterSize": after.size,
"exactChangedPixels": changed, "maxChannelDelta": maximum,
"passed": same_size and changed == 0})
if changed != 0:
print(f"FAIL {file.stem}: {changed} changed RGB pixels")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8")
passed = sum(row["passed"] for row in rows)
print(f"Desktop pixel comparison: {passed}/{len(rows)} exact matches")
if not rows or passed != len(rows):
raise SystemExit(1)
Loading