[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:
@@ -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 после подписания гейт-оферты ЦПП Стола заказов`,
|
||||
);
|
||||
}
|
||||
};
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
BIN
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 проверяет:
|
||||

|
||||
|
||||
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. Попытка открыть «Стол заказов» до подписания соглашений
|
||||
|
||||
- Все последующие входы на Стол заказов идут напрямую — гейт не показывается.
|
||||
- Подписанная оферта видна в кабинете пайщика, раздел «Подписанные документы», категория «Целевые потребительские программы».
|
||||
- В случае расторжения ЦПП пайщиком (отдельный процесс) — подпись отзывается, и при следующем входе гейт покажется снова.
|
||||

|
||||
|
||||
## Источник истины
|
||||
Что произойдёт, если Екатерина в этот момент попробует руками вбить
|
||||
адрес `/<coopname>/market/catalog` или кликнуть по ссылке на Стол заказов?
|
||||
|
||||
Запись о подписи живёт в общем реестре кооперативной платформы `agreements` — там же, где подписи на все остальные оферты (Кошелёк, Благорост, политика ПД и т. д.). Стол заказов не дублирует у себя факт подписи: каждый запрос «можно ли пайщику оформить Заказ» backend сверяет с центральным `agreements`, чтобы избежать рассинхронизации между расширениями.
|
||||
Внутри сценария мы программно делаем `page.goto('/voskhod/market/catalog')`
|
||||
**до** подписания каких-либо соглашений. URL действительно меняется —
|
||||
адресная строка показывает `/voskhod/market/catalog`. Но визуально пользователь
|
||||
видит **то же самое Положение о ЦПП «Цифровой кошелёк»**, что и на первом
|
||||
кадре: гейт онбординга не отпускает фокус.
|
||||
|
||||
Это и есть суть L3 в текущей реализации: **отдельного гейт-диалога
|
||||
«ЦПП Стола заказов» в виде full-screen карточки `«Подписать и продолжить»` /
|
||||
`«Отмена»` пока нет**. Платформенный гейт ЦПП Кошелька закрывает любые
|
||||
попытки навигации в защищённые разделы, включая `/market/*`.
|
||||
|
||||
!!!note "Платформенный гейт vs гейт расширения"
|
||||
Гейт ЦПП Кошелька — это требование **самой платформы**: без подписания
|
||||
Положения о Цифровом кошельке пайщик не сможет открыть ни один стол.
|
||||
Стол заказов наследует это поведение бесплатно — отдельный гейт
|
||||
расширения не требуется до тех пор, пока у Стола заказов нет собственной
|
||||
отдельной оферты, требующей подписи поверх общих.
|
||||
|
||||
## 3. Каталог Стола заказов после подписания всех соглашений
|
||||
|
||||

|
||||
|
||||
Сценарий программно нажимает «Подписать» во всех четырёх онбординг-диалогах
|
||||
(Положение ЦПП Кошелька + пользовательское соглашение + политика
|
||||
конфиденциальности + согласие на ЭП). После того как стек диалогов
|
||||
исчерпан, мы снова делаем `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
|
||||
|
||||
Reference in New Issue
Block a user