Files
shop-server/docs/superpowers/plans/2026-05-24-admin-test-checklist.md
T
2026-05-24 17:07:46 +05:00

24 KiB

Admin Test Checklist Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add an admin page with a manual testing checklist where each check can be marked as passed/failed with auto-timestamp.

Architecture: Hardcoded checklist items in shared constants, Prisma model for results, Fastify API route (admin-only), React admin page with MUI Accordion grouped by section.

Tech Stack: Prisma (SQLite), Fastify, React + MUI, @tanstack/react-query, apiClient (axios)


Task 1: Prisma Migration — ChecklistResult Model

Files:

  • Modify: server/prisma/schema.prisma (add model at end)

  • Step 1: Add ChecklistResult model to schema

Append to server/prisma/schema.prisma after the last model:

/// Результат ручной проверки тест-чеклиста
model ChecklistResult {
  id        String   @id @default(cuid())
  itemKey   String   @unique
  passed    Boolean
  checkedAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
  • Step 2: Run migration
cd /mnt/d/my_projects/shop/server && npx prisma migrate dev --name add_checklist_result

Expected: Migration created and applied successfully.

  • Step 3: Commit
git add server/prisma/schema.prisma server/prisma/migrations/
git commit -m "feat: add ChecklistResult model for manual test checklist"

Task 2: Shared Constants — Checklist Items

Files:

  • Create: shared/constants/test-checklist-items.js

  • Create: shared/constants/test-checklist-items.d.ts

  • Step 1: Create the JS file with all checklist items

Create shared/constants/test-checklist-items.js:

export const TEST_CHECKLIST_ITEMS = Object.freeze([
  // Авторизация
  { key: 'auth.register-email', section: 'Авторизация', action: 'Зарегистрироваться по email', expectedResult: 'Код приходит на почту, аккаунт создаётся' },
  { key: 'auth.login-password', section: 'Авторизация', action: 'Войти по паролю', expectedResult: 'Корректный пароль пускает, неправильный — ошибка' },
  { key: 'auth.oauth-vk', section: 'Авторизация', action: 'Войти через OAuth VK', expectedResult: 'Редирект на VK, callback, авторизация успешна' },
  { key: 'auth.oauth-yandex', section: 'Авторизация', action: 'Войти через OAuth Yandex', expectedResult: 'Редирект на Yandex, callback, авторизация успешна' },
  { key: 'auth.reset-password', section: 'Авторизация', action: 'Сбросить пароль', expectedResult: 'Письмо приходит, ссылка работает, пароль меняется' },
  { key: 'auth.logout', section: 'Авторизация', action: 'Выйти из аккаунта', expectedResult: 'Сессия очищается, редирект на страницу входа' },

  // Каталог и товары
  { key: 'catalog.homepage', section: 'Каталог и товары', action: 'Открыть главную страницу', expectedResult: 'Слайдер грузится, товары отображаются' },
  { key: 'catalog.filters', section: 'Каталог и товары', action: 'Применить фильтры', expectedResult: 'Фильтры по категории, цене, материалам работают' },
  { key: 'catalog.product-page', section: 'Каталог и товары', action: 'Открыть страницу товара', expectedResult: 'Фото, описание, цена, кнопка "В корзину" отображаются' },
  { key: 'catalog.seo', section: 'Каталог и товары', action: 'Проверить SEO-метаданные', expectedResult: 'Title, meta, slug корректные' },

  // Корзина
  { key: 'cart.add', section: 'Корзина', action: 'Добавить товар в корзину', expectedResult: 'Счётчик корзины обновляется' },
  { key: 'cart.change-qty', section: 'Корзина', action: 'Изменить количество товара', expectedResult: 'Сумма пересчитывается' },
  { key: 'cart.remove', section: 'Корзина', action: 'Удалить товар из корзины', expectedResult: 'Товар убирается, сумма пересчитывается' },

  // Чекаут
  { key: 'checkout.address', section: 'Чекаут', action: 'Выбрать адрес доставки', expectedResult: 'Можно выбрать из сохранённых или добавить новый' },
  { key: 'checkout.delivery', section: 'Чекаут', action: 'Выбрать способ доставки', expectedResult: 'Почта, OZON, Яндекс, 5post — доступны' },
  { key: 'checkout.payment', section: 'Чекаут', action: 'Выбрать способ оплаты', expectedResult: 'Онлайн / при получении — доступны' },
  { key: 'checkout.comment', section: 'Чекаут', action: 'Добавить комментарий к заказу', expectedResult: 'Поле работает, текст сохраняется' },
  { key: 'checkout.create', section: 'Чекаут', action: 'Создать заказ', expectedResult: 'Заказ создаётся, статус DRAFT' },

  // Оплата
  { key: 'payment.yookassa', section: 'Оплата', action: 'Оплатить через ЮKassa', expectedResult: 'Редирект на оплату, webhook обрабатывается' },
  { key: 'payment.status', section: 'Оплата', action: 'Проверить статус платежа', expectedResult: 'Статус обновляется после webhook' },

  // Профиль пользователя
  { key: 'profile.avatar', section: 'Профиль пользователя', action: 'Управление аватаром', expectedResult: 'Загрузка, отображение, удаление работают' },
  { key: 'profile.settings', section: 'Профиль пользователя', action: 'Изменить настройки профиля', expectedResult: 'Email, имя, способы входа обновляются' },
  { key: 'profile.addresses', section: 'Профиль пользователя', action: 'Управление адресами', expectedResult: 'Добавление, редактирование, удаление, по умолчанию' },
  { key: 'profile.orders', section: 'Профиль пользователя', action: 'Просмотр заказов', expectedResult: 'Список, детали, статусы отображаются' },
  { key: 'profile.messages', section: 'Профиль пользователя', action: 'Сообщения по заказу', expectedResult: 'Отправка, получение, read state работают' },
  { key: 'profile.notifications', section: 'Профиль пользователя', action: 'Настройки уведомлений', expectedResult: 'Вкл/выкл каналов работают' },
  { key: 'profile.delete-account', section: 'Профиль пользователя', action: 'Удалить аккаунт', expectedResult: 'Данные удаляются' },

  // Админ — Товары
  { key: 'admin-products.list', section: 'Админ — Товары', action: 'Открыть список товаров', expectedResult: 'Пагинация, поиск работают' },
  { key: 'admin-products.create', section: 'Админ — Товары', action: 'Создать товар', expectedResult: 'Все поля сохраняются, фото загружаются, публикация работает' },
  { key: 'admin-products.edit', section: 'Админ — Товары', action: 'Редактировать товар', expectedResult: 'Изменения сохраняются' },
  { key: 'admin-products.delete', section: 'Админ — Товары', action: 'Удалить товар', expectedResult: 'Товар удаляется' },
  { key: 'admin-products.images', section: 'Админ — Товары', action: 'Управление изображениями товара', expectedResult: 'Добавление, сортировка, удаление работают' },

  // Админ — Категории
  { key: 'admin-categories.crud', section: 'Админ — Категории', action: 'CRUD категорий', expectedResult: 'Создание, редактирование, удаление, сортировка работают' },

  // Админ — Заказы
  { key: 'admin-orders.list', section: 'Админ — Заказы', action: 'Открыть список заказов', expectedResult: 'Фильтрация по статусу, внимание отображается' },
  { key: 'admin-orders.details', section: 'Админ — Заказы', action: 'Открыть детали заказа', expectedResult: 'Состав, статус, смена статуса работают' },
  { key: 'admin-orders.messages', section: 'Админ — Заказы', action: 'Ответить на сообщение заказа', expectedResult: 'Сообщение отправляется пользователю' },

  // Админ — Отзывы
  { key: 'admin-reviews.list', section: 'Админ — Отзывы', action: 'Открыть список отзывов', expectedResult: 'Фильтрация pending/approved/rejected работает' },
  { key: 'admin-reviews.moderate', section: 'Админ — Отзывы', action: 'Модерировать отзыв', expectedResult: 'Approve/reject работают' },

  // Админ — Пользователи
  { key: 'admin-users.list', section: 'Админ — Пользователи', action: 'Открыть список пользователей', expectedResult: 'Email, дата регистрации отображаются' },
  { key: 'admin-users.orders', section: 'Админ — Пользователи', action: 'Просмотр заказов пользователя', expectedResult: 'Заказы пользователя отображаются' },

  // Админ — Галерея
  { key: 'admin-gallery.upload', section: 'Админ — Галерея', action: 'Управление галереей', expectedResult: 'Загрузка, удаление, использование в слайдере работают' },

  // Админ — Настройки
  { key: 'admin-settings.notifications', section: 'Админ — Настройки', action: 'Настройки уведомлений админа', expectedResult: 'Email, telegram настраиваются' },

  // Инфо-страницы
  { key: 'info.pages', section: 'Инфо-страницы', action: 'Открыть инфо-страницы', expectedResult: 'Доставка, оплата, как заказать, статусы заказов отображаются' },
  { key: 'info.legal', section: 'Инфо-страницы', action: 'Открыть юридические страницы', expectedResult: 'Политика конфиденциальности, условия использования отображаются' },

  // SSE / Realtime
  { key: 'sse.notifications', section: 'SSE / Realtime', action: 'Проверить SSE-уведомления', expectedResult: 'Уведомления приходят в реальном времени' },
])
  • Step 2: Create the TypeScript declaration file

Create shared/constants/test-checklist-items.d.ts:

export interface TestChecklistItem {
  key: string
  section: string
  action: string
  expectedResult: string
}

export declare const TEST_CHECKLIST_ITEMS: readonly TestChecklistItem[]
  • Step 3: Commit
git add shared/constants/test-checklist-items.js shared/constants/test-checklist-items.d.ts
git commit -m "feat: add test checklist items shared constants"

Task 3: Server API Route

Files:

  • Create: server/src/routes/api/admin/test-checklist.js

  • Modify: server/src/routes/api.js (register new route)

  • Step 1: Create the API route file

Create server/src/routes/api/admin/test-checklist.js:

import { prisma } from '../../../lib/prisma.js'

export async function registerAdminTestChecklistRoutes(fastify) {
  fastify.get('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async () => {
    const results = await prisma.checklistResult.findMany()
    const resultMap = {}
    for (const r of results) {
      resultMap[r.itemKey] = { passed: r.passed, checkedAt: r.checkedAt.toISOString() }
    }
    return { results: resultMap }
  })

  fastify.patch('/api/admin/test-checklist', { preHandler: [fastify.verifyAdmin] }, async (request) => {
    const { itemKey, passed } = request.body || {}
    if (!itemKey || typeof passed !== 'boolean') {
      return fastify.httpErrors.badRequest('itemKey and passed (boolean) are required')
    }

    const result = await prisma.checklistResult.upsert({
      where: { itemKey },
      create: { itemKey, passed },
      update: { passed, checkedAt: new Date() },
    })

    return { result: { itemKey: result.itemKey, passed: result.passed, checkedAt: result.checkedAt.toISOString() } }
  })

  fastify.post('/api/admin/test-checklist/reset', { preHandler: [fastify.verifyAdmin] }, async () => {
    await prisma.checklistResult.deleteMany({})
    return { ok: true }
  })
}
  • Step 2: Register the route in api.js

Read server/src/routes/api.js to find where other admin routes are registered (look for imports and calls like registerAdminNotificationRoutes).

Add import and registration following the existing pattern. Example:

import { registerAdminTestChecklistRoutes } from './api/admin/test-checklist.js'

// ... inside the registerApiRoutes function:
await registerAdminTestChecklistRoutes(fastify)
  • Step 3: Commit
git add server/src/routes/api/admin/test-checklist.js server/src/routes/api.js
git commit -m "feat: add admin test-checklist API routes"

Task 4: Client — API Layer

Files:

  • Create: client/src/entities/test-checklist/api/test-checklist-api.ts

  • Step 1: Create the API functions

Create client/src/entities/test-checklist/api/test-checklist-api.ts:

import { apiClient } from '@/shared/api/client'

export interface ChecklistResultDto {
  passed: boolean
  checkedAt: string
}

export interface TestChecklistResponse {
  results: Record<string, ChecklistResultDto>
}

export async function fetchTestChecklistResults(): Promise<TestChecklistResponse> {
  const { data } = await apiClient.get<TestChecklistResponse>('admin/test-checklist')
  return data
}

export async function updateTestChecklistItem(itemKey: string, passed: boolean): Promise<{ itemKey: string; passed: boolean; checkedAt: string }> {
  const { data } = await apiClient.patch<{ result: { itemKey: string; passed: boolean; checkedAt: string } }>('admin/test-checklist', { itemKey, passed })
  return data.result
}

export async function resetTestChecklist(): Promise<void> {
  await apiClient.post('admin/test-checklist/reset')
}
  • Step 2: Commit
git add client/src/entities/test-checklist/api/test-checklist-api.ts
git commit -m "feat: add test-checklist API client functions"

Task 5: Client — Admin Test Checklist Page

Files:

  • Create: client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx

  • Create: client/src/pages/admin-test-checklist/index.ts

  • Step 1: Create the page component

Create client/src/pages/admin-test-checklist/ui/AdminTestChecklistPage.tsx:

import { useMemo, useState } from 'react'
import Accordion from '@mui/material/Accordion'
import AccordionDetails from '@mui/material/AccordionDetails'
import AccordionSummary from '@mui/material/AccordionSummary'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Checkbox from '@mui/material/Checkbox'
import CircularProgress from '@mui/material/CircularProgress'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Typography from '@mui/material/Typography'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { TEST_CHECKLIST_ITEMS } from '@shared/constants/test-checklist-items'
import { fetchTestChecklistResults, resetTestChecklist, updateTestChecklistItem } from '@/entities/test-checklist/api/test-checklist-api'

function formatDate(iso: string): string {
  return new Date(iso).toLocaleString('ru-RU', {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  })
}

export function AdminTestChecklistPage() {
  const queryClient = useQueryClient()
  const [confirmOpen, setConfirmOpen] = useState(false)
  const [expanded, setExpanded] = useState<string | false>(false)

  const { data, isLoading } = useQuery({
    queryKey: ['admin', 'test-checklist'],
    queryFn: fetchTestChecklistResults,
  })

  const updateMutation = useMutation({
    mutationFn: ({ itemKey, passed }: { itemKey: string; passed: boolean }) => updateTestChecklistItem(itemKey, passed),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
    },
  })

  const resetMutation = useMutation({
    mutationFn: resetTestChecklist,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['admin', 'test-checklist'] })
      setConfirmOpen(false)
    },
  })

  const sections = useMemo(() => {
    const map = new Map<string, typeof TEST_CHECKLIST_ITEMS>()
    for (const item of TEST_CHECKLIST_ITEMS) {
      const existing = map.get(item.section) || []
      existing.push(item)
      map.set(item.section, existing)
    }
    return Array.from(map.entries())
  }, [])

  const results = data?.results ?? {}

  const total = TEST_CHECKLIST_ITEMS.length
  const passedCount = TEST_CHECKLIST_ITEMS.filter((i) => results[i.key]?.passed).length

  return (
    <Box>
      <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
        <Box>
          <Typography variant="h5" sx={{ fontWeight: 700 }}>
            Тест-чеклист
          </Typography>
          <Typography variant="body2" color="text.secondary">
            Пройдено: {passedCount} из {total}
          </Typography>
        </Box>
        <Button variant="outlined" color="warning" onClick={() => setConfirmOpen(true)} disabled={resetMutation.isPending}>
          Сбросить все
        </Button>
      </Stack>

      {isLoading ? (
        <Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
          <CircularProgress />
        </Box>
      ) : (
        sections.map(([section, items]) => (
          <Accordion key={section} expanded={expanded === section} onChange={(_, isExpanded) => setExpanded(isExpanded ? section : false)}>
            <AccordionSummary expandIcon={<ExpandMoreIcon />}>
              <Typography sx={{ fontWeight: 600 }}>{section}</Typography>
            </AccordionSummary>
            <AccordionDetails sx={{ px: 0 }}>
              <TableContainer>
                <Table size="small">
                  <TableHead>
                    <TableRow>
                      <TableCell padding="checkbox" sx={{ width: 56 }} />
                      <TableCell sx={{ fontWeight: 600 }}>Действие</TableCell>
                      <TableCell sx={{ fontWeight: 600 }}>Ожидаемый результат</TableCell>
                      <TableCell sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}>Дата проверки</TableCell>
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {items.map((item) => {
                      const r = results[item.key]
                      return (
                        <TableRow key={item.key} hover>
                          <TableCell padding="checkbox">
                            <Checkbox
                              checked={r?.passed ?? false}
                              onChange={(_, checked) => updateMutation.mutate({ itemKey: item.key, passed: checked })}
                              color="success"
                            />
                          </TableCell>
                          <TableCell>{item.action}</TableCell>
                          <TableCell>{item.expectedResult}</TableCell>
                          <TableCell sx={{ whiteSpace: 'nowrap', color: r ? 'text.primary' : 'text.disabled' }}>
                            {r ? formatDate(r.checkedAt) : '—'}
                          </TableCell>
                        </TableRow>
                      )
                    })}
                  </TableBody>
                </Table>
              </TableContainer>
            </AccordionDetails>
          </Accordion>
        ))
      )}

      <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
        <DialogTitle>Сбросить все проверки?</DialogTitle>
        <DialogContent>
          <DialogContentText>Все отметки будут удалены. Это действие нельзя отменить.</DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setConfirmOpen(false)}>Отмена</Button>
          <Button color="warning" onClick={() => resetMutation.mutate()} autoFocus>
            Сбросить
          </Button>
        </DialogActions>
      </Dialog>
    </Box>
  )
}
  • Step 2: Create the barrel export

Create client/src/pages/admin-test-checklist/index.ts:

export { AdminTestChecklistPage } from './ui/AdminTestChecklistPage'
  • Step 3: Commit
git add client/src/pages/admin-test-checklist/
git commit -m "feat: add admin test checklist page"

Task 6: Client — Routing & Navigation

Files:

  • Modify: client/src/pages/admin-layout/ui/AdminLayoutPage.tsx (add nav item + route)

  • Step 1: Add nav item and route to AdminLayoutPage

In client/src/pages/admin-layout/ui/AdminLayoutPage.tsx:

  1. Add import for ClipboardCheck from lucide-react:
import { Bell, ClipboardCheck, Image, LayoutGrid, ListOrdered, MessageSquare, Settings, Store, Users } from 'lucide-react'
  1. Add import for the page:
import { AdminTestChecklistPage } from '@/pages/admin-test-checklist'
  1. Add nav item to navItems array (after настройки):
{ to: '/admin/test-checklist', label: 'Тест-чеклист', icon: <ClipboardCheck /> },
  1. Add route to <Routes> (before the catch-all *):
<Route path="test-checklist" element={<AdminTestChecklistPage />} />
  • Step 2: Commit
git add client/src/pages/admin-layout/ui/AdminLayoutPage.tsx
git commit -m "feat: add test-checklist to admin navigation and routing"

Task 7: Verification

  • Step 1: Run server tests
cd /mnt/d/my_projects/shop/server && npm test

Expected: All tests pass.

  • Step 2: Run client lint
cd /mnt/d/my_projects/shop/client && npm run lint

Expected: No errors.

  • Step 3: Run client format check
cd /mnt/d/my_projects/shop/client && npm run format:check

Expected: All files formatted correctly.

  • Step 4: Run client tests
cd /mnt/d/my_projects/shop/client && npm test

Expected: All tests pass.

  • Step 5: Run client build
cd /mnt/d/my_projects/shop/client && npm run build

Expected: Build succeeds with no type errors.