Skip to content

feat(onboarding): интерактивный онбординг-флоу#611

Open
Themezv wants to merge 7 commits into
masterfrom
feat/onboarding-flow
Open

feat(onboarding): интерактивный онбординг-флоу#611
Themezv wants to merge 7 commits into
masterfrom
feat/onboarding-flow

Conversation

@Themezv

@Themezv Themezv commented Jun 13, 2026

Copy link
Copy Markdown
Member

Onboarding

Что внутри

  • FSM / slice / listeners в features/onboarding
  • Страницы и пошаговые описания в pages/onboarding
  • Все свойства шага (слои, подсветки, тексты, вид описания) — в одном stepConfig.ts
  • Регистрация reducer и listener, маршрут /onboarding (+ фикс опечатки OBBOARDINGONBOARDING)
  • Удаление устаревшего listenerModdleware.ts
  • Зависимость @fsmoothy/core

Сценарий

  • Вступительные шаги по кнопке «далее» → интерактивная выкладка карт → свободная игра с перелистыванием колоды → ходы соперника зелёными картами → раунд заканчивается, когда колода ligretto полностью опустела
  • Пока в ряду есть свободный слот, карту из колоды ligretto можно выложить туда (опциональный ход)

Тесты

  • Юнит-тесты FSM: src/features/onboarding/model/fsm.spec.ts (pnpm test:ci)
  • E2e полного прохождения без бэкенда: pnpm exec playwright test onboarding — vite поднимается сам (webServer), /auth/me стабится, приложение остаётся за авторизацией
  • Команды для агентов/разработчиков: apps/ligretto-frontend/AGENTS.md

🤖 Generated with Claude Code

Themezv and others added 2 commits June 28, 2026 00:17
Сама фича поверх подготовительных компонентов (#609) и общих
UI-примитивов (#610):
- FSM/slice/listeners в features/onboarding
- страницы и пошаговые описания в pages/onboarding
- регистрация reducer/listener, маршрут /onboarding (+ фикс опечатки
  OBBOARDING→ONBOARDING), удаление устаревшего listenerModdleware.ts
- зависимости @fsmoothy/core, @fsmoothy/graphviz
- доки onboarding_spec

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown

Storybook

const cycledInfoHooks = {
guard: (ctx: OnboardingContext) => ctx.data.stack.length === 0 && !ctx.data.isCycledInfoShown,
onLeave: (ctx: OnboardingContext) => {
ctx.data.isCycledInfoShown = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Что это?


export const initialState: OnboardingState = {
step: OnboardingStep.Opponents,
game: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

interface CardDescriptionProps {
index: 0 | 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Какой-то странный дизайн компонента получается.
Может в него просто text пробрасывать, а не брать один из 2х захардкоженных?


const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = {
[OnboardingStep.GameStartedCycledInfo]: 'Карты в стеке закончились — они перелистаются заново',
[OnboardingStep.OpponentTurnCycledInfo]: 'Карты в стеке закончились — они перелистаются заново',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут так же, я бы предложил в компонент только text пробрасывать

Comment on lines +11 to +18
const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = {
[OnboardingStep.Ligretto]: 'Это твоя колода ligretto',
[OnboardingStep.LigrettoCard]:
'Карты из колоды ligretto нужно выкладывать на свободное место в ряду. Раунд закончится, как только первый из игроков выложит все карты из колоды ligretto.',
[OnboardingStep.LigrettoAvailableCard]: 'Освободилось место в ряду. Выкладывай из колоды ligretto',
}

const TOP_ANCHORED_STEPS = new Set<OnboardingStep>([OnboardingStep.LigrettoCard])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И вот это вынести из компонентов

import { useOnboardingContainerRef } from '../targets'
import { useTargetRelativePosition } from '#shared/lib/hooks/useTargetRelativePosition'

const TEXT_BY_STEP: Partial<Record<OnboardingStep, string>> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

И тут

Comment thread apps/ligretto-frontend/src/pages/onboarding/Layer.tsx
Comment on lines +42 to +73
const DISABLED_LIGRETTO_STEPS = new Set<OnboardingStep>([
OnboardingStep.Opponents,
OnboardingStep.Playground,
OnboardingStep.Cards,
OnboardingStep.Stack,
OnboardingStep.Row,
OnboardingStep.Ligretto,
OnboardingStep.FirstCard,
OnboardingStep.StackCard,
OnboardingStep.StackUnavailableCard,
OnboardingStep.StackAvailableCard,
OnboardingStep.RowAvailableCard,
OnboardingStep.GameStarted,
OnboardingStep.GameStartedCycledInfo,
])

const STACK_DESCRIPTION_STEPS = new Set<OnboardingStep>([
OnboardingStep.Stack,
OnboardingStep.StackCard,
OnboardingStep.StackUnavailableCard,
OnboardingStep.StackAvailableCard,
])

const PLAYER_ROW_DESCRIPTION_STEPS = new Set<OnboardingStep>([OnboardingStep.Row])

const LIGRETTO_DESCRIPTION_STEPS = new Set<OnboardingStep>([
OnboardingStep.Ligretto,
OnboardingStep.LigrettoCard,
OnboardingStep.LigrettoAvailableCard,
])

const CENTERED_DESCRIPTION_STEPS = new Set<OnboardingStep>([OnboardingStep.GameStartedCycledInfo, OnboardingStep.OpponentTurnCycledInfo])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Такой подход, когда каждое свойство step'a в отдельном месте хранится, получается сложным.

Я бы предложил собрать все эти переменные в одну типа

const GameStartedStep = {
   isDescriptionCentered: false,
   hiddenOverlay: true,
   describeStack: false,
   ...
} satisfies StepConfig;

Ревью:
- fsm: удалено мёртвое поле stack, guard циклической подсказки проверяет
  реальную пустоту открытой колоды, колода перелистывается заново
- slice: initialState строится из createOnboardingGame() вместо
  дублирующейся захардкоженной копии
- все свойства шага собраны в один StepConfig (stepConfig.ts), тексты
  вынесены из description-компонентов и передаются через props,
  общая разметка пузыря — в DescriptionBubble, Layer работает от конфига

Сценарий:
- продлён финал: соперник отвечает зелёной двойкой на прокрутку колоды,
  игрок выкладывает зелёную тройку из ряда, раунд заканчивается только
  когда колода ligretto полностью опустела
- карту из колоды ligretto можно положить в свободный слот ряда
  на шагах opponentTurn и opponentTurnSecondCard (опциональный ход)
- подсказка о перелистывании привязана к колоде в руке, а не к центру
- экран результатов: текст поздравления внутри панели, без стрелки

Тесты и инфраструктура:
- юнит-тесты FSM (happy path, циклическая подсказка, зелёная цепочка,
  опциональные ходы, results только при пустой колоде)
- e2e полного прохождения онбординга без бэкенда: webServer в playwright
  config, стаб /auth/me, data-test-id хуки и page object
- listener онбординга завершается при уходе с роута и не дублируется
- AGENTS.md с командами тестирования (CLAUDE.md — симлинк)
- удалена неиспользуемая зависимость @fsmoothy/graphviz

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Themezv

Themezv commented Jul 19, 2026

Copy link
Copy Markdown
Member Author

Обновил PR по замечаниям ревью:

  • fsm.ts «Что это?» (stack) — поле было мёртвым (никогда не заполнялось, guard всегда true). Удалено; guard циклической подсказки теперь проверяет пустоту stackOpenDeck, колода перелистывается заново.
  • slice.ts дублирует fsm — да, и с расхождениями. initialState теперь строится из createOnboardingGame(), экспортируемой из fsm.
  • Разрозненные Set-ы / захардкоженные тексты / «зачем Layer» — все свойства шага собраны в stepConfig.ts (один объект на шаг, как предлагалось). Description-компоненты принимают text через props, общая разметка — в DescriptionBubble. Layer (поднимает зону над Overlay) работает от raisedLayers конфига, добавлен док-комментарий.

Сверх ревью: продлён финальный сценарий (зелёная цепочка 1→2→3, раунд кончается при пустой колоде ligretto), опциональные ходы ligretto→ряд, фикс listener-а (завершение при уходе с роута), юнит-тесты FSM и e2e полного прохождения без бэкенда (pnpm exec playwright test onboarding).

…rver

reuseExistingServer: true — playwright сам проверяет доступность приложения
по url/порту и переиспользует уже запущенный сервер (например, поднятый в CI),
а команду запускает только если никто не отвечает. Проверка по переменной CI
ломала e2e-джобу: CI поднимает фронтенд сам, и порт 5173 уже занят.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
})
}, [game.players, results])

if (step !== OnboardingStep.Result) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Удалить. За отрисовку или нет должен отвечать родительский компонент

navigate(routes.HOME)
}, [navigate])

const players = useMemo(() => {

@Themezv Themezv Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это reselect селектор. Нужно сделать из этого селектор.

* The onboarding flow itself needs no backend, but the whole app is gated by
* authorization. Stub the auth endpoint so the test runs without a backend.
*/
const stubAuth = async (page: Page) => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Стаб надо будет удалить.


## Conventions

- Code comments and agent docs (this file) — English only. UI copy — Russian.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это поднять в корневой Agents.md
Пункт про UI copy убрать.

- `pnpm exec playwright test onboarding` — onboarding e2e. **No backend required**: playwright starts the vite dev server itself (`webServer` in `playwright.config.ts`) and reuses one already running on :5173; the auth request (`/auth/me`) is stubbed by the test via `page.route`.
- `pnpm e2e:start` — all e2e; `e2e/tests/game.spec.ts` requires running backends (core + gameplay) and fails without them — for an isolated frontend check run only `onboarding`.

Lint/format — from the repo root: `pnpm lint:check`, `pnpm fmt:check`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Поднять в корневой

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants