docs(site): отчёт и скриншоты Круга 4 (цена не разрывается, 320–402px, Safari)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

Автопроверка 44/44 на ширинах 320/375/390/402 × главная/pricing/signup —
скриншоты k4-*.png и скрипт site-fixes-verify-4.mjs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-12 00:35:50 +05:00
parent a0f9e0b889
commit 03c1dd98ce
18 changed files with 117 additions and 0 deletions

View file

@ -208,3 +208,45 @@ net8.0` (не `net8.0-windows`) + Avalonia (кроссплатформенный
Скриншоты: `k3-pricing-1280.png`, `k3-pricing-390.png`, `k3-about-1280.png`. Скриншоты: `k3-pricing-1280.png`, `k3-pricing-390.png`, `k3-about-1280.png`.
Автопроверка (Playwright): 5/5. Юридические реквизиты не трогал (отложены до Автопроверка (Playwright): 5/5. Юридические реквизиты не трогал (отложены до
продакшна). Прод-сайт не трогал. продакшна). Прод-сайт не трогал.
---
## Круг 4 (прогон этапа 1 в Safari на iPhone 17, ширина 402 — 12.10)
Круг 3 закрыл разрыв цены **только на /pricing**. Прогон в Safari (симулятор
iPhone 17) нашёл ещё два места, где «сумма + ₸ + /мес» рвётся по строкам:
1. **Главная, строка под кнопками героя** — «Подписка от 5 000 ₸/мес»: «5» на
одной строке, «000 ₸/мес» на следующей.
2. **/signup после конфигуратора «Бизнес»** — итог «14 000 ₸/мес» рвался после
«₸/» (слэш — точка переноса), плитки тарифов — так же.
Исправлено **системно**, а не точечно (commit `a0f9e0b`, деплой только на тест
`test.food-market.kz`, статус `OK`):
- **`lib/pricing.ts` — единый источник правды:**
- `fmtKzt` теперь ставит неразрывные пробелы (U+00A0) между разрядами и перед
«₸» → сумма с «₸» никогда не рвётся;
- новый `fmtKztMonth` = сумма + «₸» + «/мес» одним куском: word-joiner (U+2060)
вокруг «/» запрещает перенос на слэше.
- **`SignupForm.tsx`** — плитки «Старт»/«Бизнес» и итог «Ваша конфигурация
„Бизнес“» используют `fmtKztMonth` (+ `whitespace-nowrap` на итоге).
- **`index.astro`** — hero-строка и карточки «Старт»/«Бизнес» (`whitespace-nowrap`
+ `&nbsp;`); «от» в «Бизнес» остаётся переносимым, цена — нет.
- **`BusinessTariffBuilder.tsx`** — подпись «База 10 000 ₸» обёрнута в `nowrap`.
- **Контент `kb/billing.md`, `kb/faq.md`, `blog/launch.md`, `FAQ.tsx`** — суммы
с nbsp + word-joiner, чтобы цена не рвалась и в тексте статей/ответов.
**Автопроверка (Playwright, Safari-UA iPhone 17):** ширины **320, 375, 390, 402**
× страницы **главная, /pricing, /signup?plan=start, /signup после «Бизнес»** —
**44/44 passed**. Высоты строк цен: hero 15px, плитки 16px, итог «Бизнес» 20px,
конструктор 36px — везде **одна строка**. Горизонтальной прокрутки нет ни на одной
ширине. Текст итога «Бизнес» = `14 000 ₸⁠/⁠мес` (неразрывный).
Скриншоты (`docs/site-fixes-2026-10-11/`): `k4-home-{320,375,390,402}.png`,
`k4-pricing-*.png`, `k4-signup-start-*.png`, `k4-signup-biz-*.png`.
Скрипт: `tests/e2e/tools/site-fixes-verify-4.mjs`.
Остальное в прогоне владельца чисто (меню, «Назад», тарифы, выбор
Старт/Бизнес/Сеть, нет горизонтальной прокрутки на 25 страницах) — не трогал.
Юридические реквизиты не трогал. Прод-сайт не трогал. Весы — пауза в силе.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View file

@ -0,0 +1,75 @@
// Круг 4: проверка, что «сумма + ₸ + /мес» нигде не разрывается по строкам.
// Ширины из прогона владельца в Safari: 320, 375, 390, 402.
import { chromium } from '@playwright/test'
import { mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
const BASE = process.env.SITE_URL ?? 'https://test.food-market.kz'
const OUT = resolve('/home/nns/food-market', 'docs/site-fixes-2026-10-11')
mkdirSync(OUT, { recursive: true })
const WIDTHS = [320, 375, 390, 402]
const results = []
const ok = (n, c, d = '') => { results.push({ n, c: !!c, d }); console.log(`${c ? '✓' : '✗'} ${n} ${d}`) }
// высота одной строки: text-xs ~16px, text-3xl ~36px, text-sm ~20px. Один ряд => ниже порога.
const oneLine = (box, max) => box && box.height > 0 && box.height <= max
const b = await chromium.launch({ args: ['--ignore-certificate-errors'] })
for (const w of WIDTHS) {
const ctx = await b.newContext({
viewport: { width: w, height: 900 }, deviceScaleFactor: 2,
ignoreHTTPSErrors: true, isMobile: true, hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
})
const p = await ctx.newPage()
// нет горизонтальной прокрутки
const noHScroll = async (label) => {
const over = await p.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
ok(`${label}@${w}: нет горизонтальной прокрутки`, over <= 1, `overflow=${over}`)
}
// ---- Главная ----
await p.goto(`${BASE}/`, { waitUntil: 'networkidle' }); await p.waitForTimeout(400)
// hero-строка: span с «5 000 ₸/мес»
const hero = await p.locator('p:has-text("Подписка от") span').first().boundingBox().catch(() => null)
ok(`home@${w}: hero «5 000 ₸/мес» одной строкой`, oneLine(hero, 24), hero ? `h=${Math.round(hero.height)}` : 'нет')
// карточки тарифов: nowrap-спаны внутри цен
const startCard = await p.locator('section:has(h2:has-text("Простые тарифы")) p.text-3xl').first().boundingBox().catch(() => null)
ok(`home@${w}: карточка «Старт» цена одной строкой`, oneLine(startCard, 48), startCard ? `h=${Math.round(startCard.height)}` : 'нет')
await p.screenshot({ path: resolve(OUT, `k4-home-${w}.png`), fullPage: true })
await noHScroll('home')
// ---- /pricing ----
await p.goto(`${BASE}/pricing`, { waitUntil: 'networkidle' }); await p.waitForTimeout(600)
const prBuilder = await p.locator('#configurator .font-extrabold').first().boundingBox().catch(() => null)
ok(`pricing@${w}: сумма конструктора одной строкой`, oneLine(prBuilder, 48), prBuilder ? `h=${Math.round(prBuilder.height)}` : 'нет')
await p.screenshot({ path: resolve(OUT, `k4-pricing-${w}.png`), fullPage: true })
await noHScroll('pricing')
// ---- /signup?plan=start ----
await p.goto(`${BASE}/signup?plan=start`, { waitUntil: 'networkidle' }); await p.waitForTimeout(600)
const tileStart = await p.locator('label:has-text("Старт") .whitespace-nowrap').first().boundingBox().catch(() => null)
ok(`signup-start@${w}: плитка «Старт» цена одной строкой`, oneLine(tileStart, 24), tileStart ? `h=${Math.round(tileStart.height)}` : 'нет')
await p.screenshot({ path: resolve(OUT, `k4-signup-start-${w}.png`), fullPage: true })
await noHScroll('signup-start')
// ---- /signup после конфигуратора «Бизнес» (14 000 ₸/мес) ----
await p.goto(`${BASE}/signup?plan=business&shops=3&cashes=1&warehouses=1&employees=5`, { waitUntil: 'networkidle' }); await p.waitForTimeout(700)
const sumBiz = await p.locator('span.font-bold.whitespace-nowrap').first().boundingBox().catch(() => null)
ok(`signup-biz@${w}: итог «14 000 ₸/мес» одной строкой`, oneLine(sumBiz, 24), sumBiz ? `h=${Math.round(sumBiz.height)}` : 'нет')
const tileBiz = await p.locator('label:has-text("Бизнес") .whitespace-nowrap').first().boundingBox().catch(() => null)
ok(`signup-biz@${w}: плитка «Бизнес» цена одной строкой`, oneLine(tileBiz, 24), tileBiz ? `h=${Math.round(tileBiz.height)}` : 'нет')
// проверим, что текст действительно «14 000 ₸/мес»
const bizTxt = await p.locator('span.font-bold.whitespace-nowrap').first().innerText().catch(() => '')
ok(`signup-biz@${w}: сумма = 14 000 ₸/мес`, /14[\s ]000[\s ]₸.?\/.?мес/.test(bizTxt), JSON.stringify(bizTxt))
await p.screenshot({ path: resolve(OUT, `k4-signup-biz-${w}.png`), fullPage: true })
await noHScroll('signup-biz')
await ctx.close()
}
await b.close()
const pass = results.filter(r => r.c).length
console.log(`\n[site-verify-4] ${pass}/${results.length} passed → ${OUT}`)
process.exit(pass === results.length ? 0 : 1)