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:
- Add import for
ClipboardCheckfromlucide-react:
import { Bell, ClipboardCheck, Image, LayoutGrid, ListOrdered, MessageSquare, Settings, Store, Users } from 'lucide-react'
- Add import for the page:
import { AdminTestChecklistPage } from '@/pages/admin-test-checklist'
- Add nav item to
navItemsarray (after настройки):
{ to: '/admin/test-checklist', label: 'Тест-чеклист', icon: <ClipboardCheck /> },
- 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.