diff --git a/apps/web/tests/smoke.spec.ts b/apps/web/tests/smoke.spec.ts index 57914a1..6971317 100644 --- a/apps/web/tests/smoke.spec.ts +++ b/apps/web/tests/smoke.spec.ts @@ -62,4 +62,28 @@ for (const viewport of [{ name: "desktop", width: 1280, height: 900 }, { name: " const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth })); expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width); }); + + test(`external staging queue is safe on ${viewport.name}`, async ({ page }) => { + await page.setViewportSize(viewport); + let mutations = 0; + await page.route("**/api/v1/**", async route => { + const request = route.request(); + if (request.method() !== "GET") mutations += 1; + const path = new URL(request.url()).pathname; + if (path.endsWith("/fishes")) return route.fulfill({ json: [{ slug: "pike", name_ru: "Щука" }] }); + if (path.endsWith("/waterbodies")) return route.fulfill({ json: [{ slug: "kuori", name_ru: "Куори" }] }); + if (path.endsWith("/admin/external-observations")) return route.fulfill({ json: [{ id: "10000000-0000-0000-0000-000000000001", source_system: "rf4db", source_external_id: "fixture-1", source_url: "https://rf4db.com/fixture-1", fish_name: "Pike", waterbody_name: "Kuori", x: 72, y: 84, weight_g: null, status: "staged", fish_slug: null, waterbody_slug: null, review_note: null }] }); + return route.fulfill({ status: 404 }); + }); + await page.goto("/admin/external-sources"); + await page.getByLabel("Административный токен").fill("test-token-not-sent"); + await page.getByRole("button", { name: "Открыть очередь" }).click(); + const card = page.locator(".moderation-card"); + await expect(card).toContainText("rf4db"); + await expect(card.getByLabel("Каноническая рыба")).toContainText("Щука"); + await expect(card.getByRole("button", { name: "Опубликовать" })).toBeDisabled(); + expect(mutations).toBe(0); + const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth })); + expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width); + }); } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3f62abc..ca7c22e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -78,7 +78,7 @@ - [x] Упростить форму улова, добавить помощь форматов, валидацию и сохранение значений при ошибке (пакет C UI/UX-аудита). - [x] Заменить slug-фильтры справочниками, добавить сброс и самодостаточные мобильные строки рекордов (пакет D UI/UX-аудита; detail уже адаптивен). - [x] Завершить accessibility/admin safety пакет с воспроизводимыми axe/Lighthouse-командами (пакет E). -- [ ] Добавить smoke-проверку административной очереди внешних источников на desktop/mobile без публикации реальных записей. +- [x] Добавить smoke-проверку административной очереди внешних источников на desktop/mobile: API-фикстура, неполная запись, disabled publish, ноль mutation-запросов и отсутствие overflow. - [x] Обновить README: актуальный статус, архитектура, группы переменных окружения, импорт, модерация, backup/restore, эксплуатация логов и известные ограничения. - [x] Выбрать лицензию кода `AGPL-3.0-only` и оформить отдельную политику использования, минимизации, хранения и удаления данных (`LICENSE`, `docs/data-policy.md`). - [ ] Завершить production-профиль для `rf4spotter.ru`: приложение и `deploy/preflight.sh` готовы; внешняя проверка 7 сентября не установила соединение с обоими доменами, требуются IP целевого сервера, DNS, запуск и канал уведомлений (`docs/deployment-status.md`). @@ -119,9 +119,8 @@ Технический production-контур, health/readiness, backup/restore и безопасные логи готовы. Следующие пункты выполняются строго по одному: -1. добавить smoke-проверку административного staging без публикации; -2. подготовить сценарий и измеримые критерии закрытой альфы; -3. мониторинг, DNS/TLS и проверка production-профиля на целевом сервере. +1. подготовить сценарий и измеримые критерии закрытой альфы; +2. мониторинг, DNS/TLS и проверка production-профиля на целевом сервере. После каждого пункта необходимо: