[598][@ant] docs(marketplace): онбординг L3 — 3 e2e скриншота гейта Стола заказов

Документирует фактическое поведение свежей пайщицы Екатерины при первом
входе на Стол заказов: модальный стек 4 общих соглашений (ЦПП Кошелёк +
3 базовых) держит любую навигацию в /market/*; отдельной гейт-оферты
расширения сегодня нет; после подписания каталог открывается пустой
в контексте «Стола пайщика». Проза переписана с гипотетического
описания на наблюдаемые факты со скриншотов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-20 23:53:19 +00:00
parent 4bf9d3cd7b
commit 14da4a4de5
5 changed files with 248 additions and 16 deletions
@@ -0,0 +1,146 @@
// Сценарий: Onboarding L3 — гейт первого входа на Стол заказов.
// Цель: показать реальный UX свежей пайщицы Екатерины при первом посещении
// marketplace. Снимаем последовательно: стартовый экран после логина (стек
// диалогов соглашений), попытку открыть /market/catalog до подписания
// соглашений и итоговое состояние после подписания всех соглашений.
//
// Фикстура: ekaterina / Екатерина Александровна Смирнова — создаётся
// автоматически через bin/shoot.mjs (KNOWN_FIXTURES) при первом прогоне.
// Если фикстура уже есть на стенде с раннего сценария — гейт пройти не
// получится (она уже подписала всё). Тогда лучше прогнать с --reboot.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { cleanViteOverlays, env, loginAs } from '../../../lib/harness.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const loadFixture = (username) =>
JSON.parse(
fs.readFileSync(path.resolve(__dirname, `../../../state/participants/${username}.json`), 'utf8'),
);
export const meta = {
title: 'Онбординг L3 — гейт первого входа на Стол заказов',
docPath: 'new/marketplace/onboarding/extension-gate.md',
assetsDir: 'assets/new/marketplace/onboarding/extension-gate',
role: 'user',
fixture: 'ekaterina',
fixtures: ['ekaterina'],
};
// Снимает один диалог подписания: возвращает {present, title} —
// если диалога нет, present=false; если есть — present=true и заголовок диалога.
async function inspectTopDialog(page) {
return page.evaluate(() => {
const portals = Array.from(document.querySelectorAll('[id^="q-portal--dialog--"]'))
.filter((p) => getComputedStyle(p).display !== 'none');
if (portals.length === 0) return { present: false };
const top = portals[portals.length - 1];
// q-card-section с заголовком обычно идёт первой
const titleEl = top.querySelector('.q-card-section .text-h6, .q-card-section h6, .text-h6');
const title = titleEl?.textContent?.trim()
?? top.querySelector('.q-card-section')?.textContent?.trim()?.slice(0, 80)
?? '';
const hasSignButton = Array.from(top.querySelectorAll('button'))
.some((b) => b.textContent?.trim() === 'Подписать' && !b.disabled);
return { present: true, title, hasSignButton };
});
}
// Подписать один верхний диалог; возвращает true если кликнул.
async function signOneDialog(page) {
const clicked = await page.evaluate(() => {
const portals = Array.from(document.querySelectorAll('[id^="q-portal--dialog--"]'))
.filter((p) => getComputedStyle(p).display !== 'none');
if (portals.length === 0) return false;
const top = portals[portals.length - 1];
const btn = Array.from(top.querySelectorAll('button'))
.find((b) => b.textContent?.trim() === 'Подписать' && !b.disabled);
if (!btn) return false;
btn.scrollIntoView({ block: 'center', behavior: 'instant' });
btn.click();
return true;
});
if (clicked) await page.waitForTimeout(3500);
return clicked;
}
export default async ({ page, shot }) => {
const fixture = loadFixture('ekaterina');
await loginAs(page, fixture);
await page.evaluate(() => localStorage.setItem('harness:noBranchOverlay', '1'));
// Дать UI собрать первый диалог (формируем документ → готовый PDF).
await page.waitForFunction(
() => !document.body.innerText.includes('Формируем документ'),
{ timeout: 30000 },
).catch(() => {});
await page.waitForTimeout(1500);
await cleanViteOverlays(page);
// --- 01. Стек диалогов сразу после логина новой пайщицы ---
const first = await inspectTopDialog(page);
await shot(
page,
'01-onboarding-stack',
`Стек онбординг-диалогов сразу после первого входа Екатерины. Верхний диалог: «${first.title || 'нет'}»`,
);
// --- 02. Попытка зайти в Стол заказов до подписания каких-либо соглашений ---
// Открываем /market/catalog в новой вкладке такого же контекста через page.goto.
// Если расширение строит гейт — увидим его поверх онбординг-диалогов или
// увидим редирект на /user/wallet с висящим стеком соглашений.
await page.goto(`${env.BASE_URL}/${env.COOPNAME}/market/catalog`, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
await page.waitForTimeout(2500);
await cleanViteOverlays(page);
const second = await inspectTopDialog(page);
await shot(
page,
'02-market-before-signing',
`Попытка открыть /market/catalog ДО подписания соглашений. URL: \`${page.url()}\`. Верхний диалог: «${second.title || 'нет'}»`,
);
// --- 03. Подписываем все доступные диалоги-соглашения (общие + ЦПП Кошелёк) ---
// signAllAgreements: до 8 итераций; каждый клик «Подписать» закрывает
// верхний диалог стека.
let dialogsSigned = 0;
for (let i = 0; i < 8; i++) {
const ok = await signOneDialog(page);
if (!ok) break;
dialogsSigned += 1;
}
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
await cleanViteOverlays(page);
// --- 04. Каталог Стола заказов после подписания всех соглашений ---
// Если есть отдельный гейт ЦПП Стола заказов — он должен показаться сейчас
// как новый диалог. Если нет — каталог должен открыться.
await page.goto(`${env.BASE_URL}/${env.COOPNAME}/market/catalog`, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
await page.waitForTimeout(3000);
await cleanViteOverlays(page);
const third = await inspectTopDialog(page);
await shot(
page,
'03-market-after-signing',
`Каталог Стола заказов ПОСЛЕ подписания соглашений. URL: \`${page.url()}\`. Подписано диалогов: ${dialogsSigned}. Верхний диалог: «${third.title || 'нет'}»`,
);
// --- 05. Если остался гейт — попробуем подписать его и снять финальное состояние ---
if (third.present && third.hasSignButton) {
await signOneDialog(page);
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2500);
await cleanViteOverlays(page);
await shot(
page,
'04-market-after-gate-signed',
`Финальное состояние /market/catalog после подписания гейт-оферты ЦПП Стола заказов`,
);
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -1,37 +1,123 @@
---
title: Онбординг L3 — гейт первого входа на Стол заказов
generated_by: docs-harness
scenario: marketplace/onboarding/extension-gate
tags:
- Пайщик
---
# L3 — Гейт первого входа на Стол заказов
Если пайщик пропустил выбор ЦПП «Стол заказов» при регистрации ([L2][l2]) и заходит на стол впервые, его встречает **гейт-экран**: единый диалог с превью оферты ЦПП и кнопкой «Подписать».
Свежая пайщица **Екатерина Александровна Смирнова** только что приняла свой
паевой взнос и впервые зашла в личный кабинет. Что она увидит, если попытается
открыть Стол заказов до подписания обязательных соглашений? Этот раздел
документирует наблюдаемое поведение в текущей сборке.
Это поведение защищает кооператив от ситуации, когда пайщик пытается оформить Заказ или опубликовать Предложение, не присоединившись к программе.
!!!info "Что такое L3"
L1 — кооператив подключает ЦПП «Стол заказов» (председатель + совет).
L2 — пайщик выбирает ЦПП при регистрации.
L3 — то, что описано здесь: **гейт первого входа**, который не позволит
пайщику работать со столом, пока он не подпишет обязательные ЦПП-оферты.
## Как работает гейт
## 1. Что появляется сразу после первого входа
При переходе на любую страницу Стола заказов backend проверяет:
![Стек онбординг-диалогов сразу после первого входа Екатерины: верхний диалог «Прочитайте и подпишите документ» — Положение о ЦПП «Цифровой кошелёк»](/assets/new/marketplace/onboarding/extension-gate/01-onboarding-stack.png)
1. **Активна ли в кооперативе ЦПП «Стол заказов»** — если совет ещё не подключил программу ([L1][l1]), стол вообще недоступен, показывается заглушка «Программа не подключена».
2. **Подписана ли пайщиком оферта ЦПП** — backend обращается к общему реестру `agreements`. Если подпись найдена — стол открывается. Если нет — показывается гейт.
Сразу после успешного входа `ekaterina.smirnova@example.com` (с её WIF) пайщица
попадает не на кошелёк, а на **модальный стек подписания документов**.
Верхняя зелёная полоса с надписью **«Прочитайте и подпишите документ»** —
это и есть онбординг-гейт платформы, общий для всех ЦПП.
Гейт — это full-screen диалог-превью оферты с двумя кнопками:
В первой карточке стека показано **Положение о ЦПП «ЦИФРОВОЙ КОШЕЛЁК»**:
- УТВЕРЖДЕНО Протоколом Совета № 10-04-2024 ПК «Восход» от 10 апреля 2024 г.
- Программа определяет «условия удовлетворения потребностей пайщиков
ПК "Восход" в обеспечении эффективности взаимодействия, учета и удобства
операций по взаимодействию с Кооперативом, прозрачности и безопасности
взаиморасчетов по целевым потребительским программам Кооператива».
- Разработана на основании Гражданского кодекса РФ и Закона РФ от 19.06.1992
N 3085-1 «О потребительской кооперации».
- **Подписать и продолжить** — оферта подписывается ЭП пайщика, запись попадает в `agreements`, диалог закрывается, пайщик попадает на запрошенный стол.
- **Отмена** — возврат в личный кабинет (на стол пайщика). Стол заказов не открывается.
Под текстом документа — кнопка **«Подписать»**, активная после прокрутки.
Это первый из четырёх онбординг-диалогов; за ним пайщик увидит пользовательское
соглашение, политику конфиденциальности и согласие на ЭП.
## Когда гейт пропадает
!!!warning "Гейт модальный"
Пайщик НЕ может закрыть этот диалог крестиком и НЕ может перейти в меню
бокового сайдбара, пока документ не подписан. Стек диалогов открывается
автоматически при первом логине новой пайщицы и удерживает фокус.
Гейт показывается **ровно один раз**. После подписи оферты:
## 2. Попытка открыть «Стол заказов» до подписания соглашений
- Все последующие входы на Стол заказов идут напрямую — гейт не показывается.
- Подписанная оферта видна в кабинете пайщика, раздел «Подписанные документы», категория «Целевые потребительские программы».
- В случае расторжения ЦПП пайщиком (отдельный процесс) — подпись отзывается, и при следующем входе гейт покажется снова.
![Попытка открыть /market/catalog ДО подписания соглашений — гейт удерживает пайщика на том же экране подписания ЦПП Цифрового кошелька](/assets/new/marketplace/onboarding/extension-gate/02-market-before-signing.png)
## Источник истины
Что произойдёт, если Екатерина в этот момент попробует руками вбить
адрес `/<coopname>/market/catalog` или кликнуть по ссылке на Стол заказов?
Запись о подписи живёт в общем реестре кооперативной платформы `agreements` — там же, где подписи на все остальные оферты (Кошелёк, Благорост, политика ПД и т. д.). Стол заказов не дублирует у себя факт подписи: каждый запрос «можно ли пайщику оформить Заказ» backend сверяет с центральным `agreements`, чтобы избежать рассинхронизации между расширениями.
Внутри сценария мы программно делаем `page.goto('/voskhod/market/catalog')`
**до** подписания каких-либо соглашений. URL действительно меняется —
адресная строка показывает `/voskhod/market/catalog`. Но визуально пользователь
видит **то же самое Положение о ЦПП «Цифровой кошелёк»**, что и на первом
кадре: гейт онбординга не отпускает фокус.
Это и есть суть L3 в текущей реализации: **отдельного гейт-диалога
«ЦПП Стола заказов» в виде full-screen карточки `«Подписать и продолжить»` /
`«Отмена»` пока нет**. Платформенный гейт ЦПП Кошелька закрывает любые
попытки навигации в защищённые разделы, включая `/market/*`.
!!!note "Платформенный гейт vs гейт расширения"
Гейт ЦПП Кошелька — это требование **самой платформы**: без подписания
Положения о Цифровом кошельке пайщик не сможет открыть ни один стол.
Стол заказов наследует это поведение бесплатно — отдельный гейт
расширения не требуется до тех пор, пока у Стола заказов нет собственной
отдельной оферты, требующей подписи поверх общих.
## 3. Каталог Стола заказов после подписания всех соглашений
![Каталог Стола заказов открыт пустым: левый сайдбар «Стол пайщика», по центру «Каталог» с фильтром «Все 0» и плейсхолдером «Ничего не найдено — в каталоге пока нет активных предложений»](/assets/new/marketplace/onboarding/extension-gate/03-market-after-signing.png)
Сценарий программно нажимает «Подписать» во всех четырёх онбординг-диалогах
(Положение ЦПП Кошелька + пользовательское соглашение + политика
конфиденциальности + согласие на ЭП). После того как стек диалогов
исчерпан, мы снова делаем `page.goto('/voskhod/market/catalog')`
и каталог открывается.
Что видно на экране:
- **Левый сайдбар — «Стол пайщика»**, не «Стол заказов». Пункты:
Кошелёк, Удостоверение, Реквизиты, Документы, Платежи, Собрания, Контакты,
Поддержка. Карточка профиля внизу — «Смирнова Екатерина Александровна»,
бейдж **«Пайщик»**, остаток кошелька **0,00 RUB** «Доступно».
- **Заголовок страницы — «Каталог»**.
- **Один-единственный фильтр — `✓ Все 0`** (счётчик предложений = 0).
- Справа сверху — селектор сортировки **«Свежие сначала»**.
- В центральной зоне — серый placeholder: иконка вопроса и текст
**«Ничего не найдено — в каталоге пока нет активных предложений»**.
Это нормальное состояние свежего стенда: ЦПП-категории были созданы
миграцией расширения (`marketplaceBootstrapV4Migration` — 9 базовых
категорий), но **активных Предложений на витрине ещё нет**, а сам стенд
не успел подключить отдельную навигацию «Стол заказов» в сайдбар для
этой роли — Екатерина видит каталог в контексте «Стола пайщика».
!!!info "Что это значит для пайщицы"
L3-гейт в текущей реализации — это **общий онбординг 4 соглашений**.
После их подписания у Екатерины уже есть доступ к каталогу
Стола заказов; никакого отдельного экрана-преграды специально для
Стола заказов система не показывает. Это сознательное упрощение
MVP: одна точка подписи документов = более быстрый онбординг.
## Что НЕ удалось снять и почему
Сценарий настроен на снимок четвёртого кадра — **финального состояния после
подписи отдельной гейт-оферты ЦПП Стола заказов** (`04-market-after-gate-signed`).
Этот кадр не был снят: после прохождения общего онбординга (4 диалога)
дополнительного диалога-гейта со стороны расширения не появилось, и
условие `third.present && third.hasSignButton` оказалось ложным.
Это **факт, а не баг сценария**: в коде эпика «Онбординг» Стола заказов
сегодня нет отдельной оферты, которую пайщик должен был бы подписывать
поверх ЦПП Кошелька. Если в будущем такая оферта появится, сценарий
автоматически снимет четвёртый кадр и инструкция будет дополнена.
[l1]: ./coop-accept-cpp.md
[l2]: ./member-pick-cpp.md