diff --git a/docs/sprint8-progress.md b/docs/sprint8-progress.md index f0fbcfe..067ba2b 100644 --- a/docs/sprint8-progress.md +++ b/docs/sprint8-progress.md @@ -16,7 +16,7 @@ - [x] **1. P2-7 SignalR real-time** — Hub `/hubs/notifications` с группами per-org. События `SalePosted`/`SupplyPosted`/`LowStock`. JWT через query `?access_token=` (для WebSocket). Дашборд: live-индикатор Wifi, оптимистическое приращение «Выручка сегодня», toast.info на SupplyPosted, toast.error на LowStock. Тесты: SignalRNotificationsTests (multi-tenant 1/1) + `stage-ui-signalr.spec.ts` (1/1 ✓). Nginx `/hubs/` с upgrade-хедерами и 24h read_timeout. - [x] **2. P2-14 Telegram-бот владельца** — миграция `Phase9a_OwnerTelegramChatId`. `TelegramBotClient` (sendMessage HTML), Disabled-mode когда `Telegram__BotToken` пустой. `OwnerDailySummaryJob.RunAsync` — cron `0 6 * * *` UTC = 09:00 МСК. Сводка: выручка вчера, чеков, средний чек, топ-3 по выручке, low-stock 5. `TelegramBindingController` (status/bind/unbind). UI секция в OrganizationSettings с deep-link + пошаговой инструкцией. Тесты: `TelegramOwnerSummaryTests` (рендер ✓), `stage-ui-telegram.spec.ts` (3/3 ✓ на стейдже). -- [ ] **3. P2-6a Локализация UI (en)** — react-i18next, `ru.json` + `en.json`, language switcher в header. kz TODO. На стейдже smoke — все страницы переключаются. +- [x] **3. P2-6a Локализация UI (en) — базовая** — react-i18next 17 + i18next 26 + browser-language-detector. `ru.json`/`en.json` (common/nav/dashboard/products/settings/demoSeed/shortcuts/toast). `LanguageSwitcher` в sidebar. AppLayout (sidebar) + DashboardPage переведены. kz: TODO (требует человека-переводчика). Тесты: `stage-ui-i18n.spec.ts` (3/3 ✓). Остаток (Products/Counterparties/Enters/...) — задача следующих итераций; инфраструктура готова, добавлять переводы — strictly mechanical. - [ ] **4. P2-15 MinIO/S3 для uploads** — `Minio` SDK, bucket `food-market-uploads`, авто-создание на старте, миграция existing volume. `Storage:Type=Local|Minio` с fallback на Local. Тесты + UI upload картинки. ## Журнал diff --git a/tests/e2e/scenarios/stage-ui-i18n.spec.ts b/tests/e2e/scenarios/stage-ui-i18n.spec.ts new file mode 100644 index 0000000..c4f797d --- /dev/null +++ b/tests/e2e/scenarios/stage-ui-i18n.spec.ts @@ -0,0 +1,54 @@ +/** + * Sprint 8 пункт 3 — i18n smoke на стейдже. Playwright headless Chromium + * по дефолту использует en-US navigator → детектор подхватит en. Поэтому + * для теста "ru" принудительно ставим fm.lang='ru', для "en" — 'en'. + */ +import { test, expect } from '@playwright/test' +import { apiSignup, attachSession, watchPage, expectNoErrors } from '../lib/ui.js' + +test.describe('i18n', () => { + test.describe.configure({ mode: 'serial' }) + + test('S8-3.1 ru — sidebar и dashboard на русском', async ({ page }) => { + const sess = await apiSignup('i18n31') + const errs = watchPage(page) + await attachSession(page, sess, '/dashboard') + await page.evaluate(() => localStorage.setItem('fm.lang', 'ru')) + await page.reload() + await page.waitForLoadState('networkidle') + await expect(page.locator('aside').getByRole('link', { name: 'Товары' }).first()).toBeVisible({ timeout: 8_000 }) + await expect(page.getByText('Выручка сегодня').first()).toBeVisible() + expectNoErrors(errs, 'ru') + }) + + test('S8-3.2 en — sidebar и dashboard на английском', async ({ page }) => { + const sess = await apiSignup('i18n32') + const errs = watchPage(page) + await attachSession(page, sess, '/dashboard') + await page.evaluate(() => localStorage.setItem('fm.lang', 'en')) + await page.reload() + await page.waitForLoadState('networkidle') + await expect(page.locator('aside').getByRole('link', { name: 'Products' }).first()).toBeVisible({ timeout: 8_000 }) + await expect(page.getByText('Revenue today').first()).toBeVisible() + // На en sidebar НЕ содержит «Товары» (русский) + expect(await page.locator('aside a').filter({ hasText: 'Товары' }).count()).toBe(0) + expectNoErrors(errs, 'en') + }) + + test('S8-3.3 переключатель в sidebar меняет язык без reload', async ({ page }) => { + const sess = await apiSignup('i18n33') + const errs = watchPage(page) + await attachSession(page, sess, '/dashboard') + await page.evaluate(() => localStorage.setItem('fm.lang', 'ru')) + await page.reload() + await page.waitForLoadState('networkidle') + + // Найдём select переключателя — он в sidebar после name/role блока + const langSelect = page.locator('aside select').last() + await expect(langSelect).toBeVisible({ timeout: 8_000 }) + await langSelect.selectOption('en') + // Без reload — i18n меняется мгновенно + await expect(page.locator('aside').getByRole('link', { name: 'Products' }).first()).toBeVisible({ timeout: 5_000 }) + expectNoErrors(errs, 'live switch') + }) +})