From 40e2275949a98cd2196e4adc8ecf209e29af6e16 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 18 May 2026 08:53:58 +0300 Subject: [PATCH] =?UTF-8?q?test:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Playwright=20UI=20smoke-=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=20(10=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BE=D0=BA=20JS-?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=BF=D0=BE=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=D0=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/ui_smoke.js — headless Chromium с mock Telegram.WebApp, проверяет загрузку, список клиентов, форму нового клиента, замеры, сборки - package.json — playwright devDependency - .claude/commands/test.md — добавлен Шаг 4 (UI Playwright) в агент тестировщика Co-Authored-By: Claude Sonnet 4.6 --- .claude/commands/test.md | 15 ++- package.json | 5 + tests/ui_smoke.js | 264 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 package.json create mode 100644 tests/ui_smoke.js diff --git a/.claude/commands/test.md b/.claude/commands/test.md index 04fec5e..96545cf 100644 --- a/.claude/commands/test.md +++ b/.claude/commands/test.md @@ -20,7 +20,13 @@ python -X utf8 tests/test_manager.py ``` Проверяет: аутентификацию, клиентов, замеры, сборки, предложения, сотрудников, отгрузки, устойчивость к плохим данным. -## Шаг 4 — Сводный отчёт +## Шаг 4 — UI Smoke (Playwright) +```bash +node tests/ui_smoke.js +``` +Проверяет: JS-ошибки на каждом экране, рендер списка клиентов, форма нового клиента, экраны замеров и сборок. Сохраняет скриншот в `tests/ui_last_run.png`. + +## Шаг 5 — Сводный отчёт Выведи в формате: @@ -28,9 +34,10 @@ python -X utf8 tests/test_manager.py ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ОТЧЁТ ТЕСТИРОВЩИКА ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - CSS-линтер ✅ / ❌ (N ошибок, N предупреждений) - Smoke API ✅ / ❌ (N/12 пройдено) - Кабинет менеджера ✅ / ❌ (N/19 пройдено) + CSS-линтер ✅ / ❌ (N ошибок, N предупреждений) + Smoke API ✅ / ❌ (N/12 пройдено) + Кабинет менеджера ✅ / ❌ (N/19 пройдено) + UI Playwright ✅ / ❌ (N/10 пройдено) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ИТОГО: ✅ МОЖНО КОММИТИТЬ ❌ НЕЛЬЗЯ — исправь замечания: diff --git a/package.json b/package.json new file mode 100644 index 0000000..8b4ada0 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "playwright": "^1.60.0" + } +} diff --git a/tests/ui_smoke.js b/tests/ui_smoke.js new file mode 100644 index 0000000..df4e2b6 --- /dev/null +++ b/tests/ui_smoke.js @@ -0,0 +1,264 @@ +/** + * UI smoke-тест MiniApp через Playwright. + * Запуск: node tests/ui_smoke.js + * + * Что проверяет: + * - Нет JS-ошибок (ReferenceError, TypeError и т.п.) на каждом экране + * - Список клиентов загружается + * - Карточка клиента открывается без ошибок + * - Форма нового клиента открывается без ошибок + * - Экраны замеров и сборок открываются без ошибок + */ + +const { chromium } = require("playwright"); +const crypto = require("crypto"); + +// ─── Конфигурация ──────────────────────────────────────────────────────────── +const BOT_TOKEN = "8281503057:AAEXmOepY8quH8E3RqOjFbgn7owV1ngnbGA"; +const ADMIN_TG_ID = 5937498515; +const MINIAPP_URL = "https://wasrusgen.github.io/zov-tech/"; +const TIMEOUT_MS = 15000; + +// ─── Генерация валидного initData ───────────────────────────────────────────── +function makeInitData() { + const user = JSON.stringify({ + id: ADMIN_TG_ID, + first_name: "Руслан", + username: "wasrusgen", + language_code: "ru", + allows_write_to_pm: true, + }); + const fields = { + auth_date: String(Math.floor(Date.now() / 1000)), + user, + }; + const dataCheckString = Object.keys(fields).sort() + .map(k => `${k}=${fields[k]}`).join("\n"); + const secretKey = crypto.createHmac("sha256", "WebAppData") + .update(BOT_TOKEN).digest(); + const hash = crypto.createHmac("sha256", secretKey) + .update(dataCheckString).digest("hex"); + return new URLSearchParams({ ...fields, hash }).toString(); +} + +// ─── Отчёт ─────────────────────────────────────────────────────────────────── +const RESULTS = []; +function pass(name, detail = "") { + RESULTS.push({ ok: true, name, detail }); + console.log(` ✅ ${name}${detail ? " — " + detail : ""}`); +} +function fail(name, detail = "") { + RESULTS.push({ ok: false, name, detail }); + console.log(` ❌ ${name}${detail ? " — " + detail : ""}`); +} +function section(title) { + console.log(`\n${"─".repeat(55)}\n ${title}\n${"─".repeat(55)}`); +} + +// ─── Вспомогательные функции ───────────────────────────────────────────────── + +/** Ждёт появления элемента на странице (или возвращает null по таймауту) */ +async function waitForSelector(page, selector, timeout = TIMEOUT_MS) { + try { + await page.waitForSelector(selector, { timeout }); + return true; + } catch { + return false; + } +} + +/** Возвращает JS-ошибки накопленные с момента последнего вызова reset */ +function makeErrorCollector(page) { + const errors = []; + page.on("pageerror", e => errors.push(e.message)); + page.on("console", msg => { + if (msg.type() === "error") errors.push(msg.text()); + }); + return { + flush() { const copy = [...errors]; errors.length = 0; return copy; }, + any() { return errors.length > 0; }, + }; +} + +// ─── Основные тесты ────────────────────────────────────────────────────────── + +async function run() { + console.log(`\n${"=".repeat(55)}`); + console.log(" UI SMOKE-ТЕСТ MiniApp (Playwright)"); + console.log(` ${MINIAPP_URL}`); + console.log(`${"=".repeat(55)}`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 390, height: 844 }, + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) TelegramBot", + }); + const page = await context.newPage(); + const errors = makeErrorCollector(page); + + const initData = makeInitData(); + const initDataUnsafe = { + user: { id: ADMIN_TG_ID, first_name: "Руслан", username: "wasrusgen", language_code: "ru" }, + auth_date: Math.floor(Date.now() / 1000), + hash: "", + }; + + // ── Инжектируем mock Telegram.WebApp ────────────────────────────────────── + await page.addInitScript(({ initData, initDataUnsafe }) => { + window.Telegram = { + WebApp: { + initData, + initDataUnsafe, + colorScheme: "dark", + themeParams: {}, + isExpanded: true, + viewportHeight: 844, + viewportStableHeight: 844, + MainButton: { show() {}, hide() {}, setText() {}, onClick() {} }, + BackButton: { show() {}, hide() {}, onClick() {} }, + HapticFeedback: { impactOccurred() {}, notificationOccurred() {} }, + ready() {}, + expand() {}, + close() {}, + showAlert(msg) { console.log("[tg.showAlert]", msg); }, + showConfirm(msg, cb) { cb(true); }, + }, + }; + }, { initData, initDataUnsafe }); + + // ── 1. Загрузка страницы ────────────────────────────────────────────────── + section("📄 Загрузка приложения"); + try { + await page.goto(MINIAPP_URL, { waitUntil: "domcontentloaded", timeout: 20000 }); + const appEl = await waitForSelector(page, "#app", 5000); + appEl ? pass("MiniApp загрузился (#app найден)") : fail("MiniApp не загрузился (#app не найден)"); + } catch (e) { + fail("Ошибка загрузки страницы", e.message); + await browser.close(); + printSummary(); + return; + } + const jsErrOnLoad = errors.flush(); + jsErrOnLoad.length === 0 + ? pass("Нет JS-ошибок при загрузке") + : fail("JS-ошибки при загрузке", jsErrOnLoad.slice(0, 2).join(" | ")); + + // Авторизуемся как менеджер + try { + await page.goto(MINIAPP_URL + "?role=manager", { waitUntil: "domcontentloaded", timeout: 15000 }); + await page.waitForTimeout(1500); + } catch { /* ок */ } + + // ── 2. Экран клиентов ───────────────────────────────────────────────────── + section("👥 Список клиентов"); + await page.evaluate(() => { location.hash = "#/clients"; }); + await page.waitForTimeout(3000); + const jsErrClients = errors.flush(); + jsErrClients.length === 0 + ? pass("Нет JS-ошибок на экране клиентов") + : fail("JS-ошибки на экране клиентов", jsErrClients.slice(0, 2).join(" | ")); + + // Проверяем что что-то отрендерилось + const hasClientList = await page.evaluate(() => + document.querySelector(".client-card, .empty, .error") !== null + ); + hasClientList + ? pass("Список клиентов отрендерился") + : fail("Список клиентов пустой (нет .client-card, .empty, .error)"); + + // ── 3. Карточка первого клиента ─────────────────────────────────────────── + section("🪪 Карточка клиента"); + const firstCard = await page.$(".client-card"); + if (firstCard) { + await firstCard.click(); + await page.waitForTimeout(3000); + const jsErrCard = errors.flush(); + jsErrCard.length === 0 + ? pass("Нет JS-ошибок при открытии карточки") + : fail("JS-ошибки в карточке клиента", jsErrCard.slice(0, 2).join(" | ")); + + const headerText = await page.evaluate(() => + document.querySelector(".podbor-title")?.textContent?.trim() || "" + ); + headerText.toLowerCase().includes("карточка") + ? pass("Заголовок карточки", headerText) + : fail("Ожидался заголовок 'Карточка клиента'", `получили: "${headerText}"`); + + const hasDetails = await page.evaluate(() => + document.querySelector(".client-detail-head, .client-quick-actions") !== null + ); + hasDetails + ? pass("Содержимое карточки отрендерилось") + : fail("Карточка пустая (нет .client-detail-head)"); + } else { + pass("Карточка клиента — пропущено (нет клиентов в списке)", "добавьте клиента для теста"); + } + + // ── 4. Форма нового клиента ─────────────────────────────────────────────── + section("➕ Форма нового клиента"); + await page.evaluate(() => { location.hash = "#/clients/new"; }); + await page.waitForTimeout(2000); + const jsErrNew = errors.flush(); + jsErrNew.length === 0 + ? pass("Нет JS-ошибок в форме нового клиента") + : fail("JS-ошибки в форме нового клиента", jsErrNew.slice(0, 2).join(" | ")); + + const hasForm = await page.evaluate(() => + document.querySelector("#fn, #ph") !== null + ); + hasForm + ? pass("Форма отрендерилась (поля ФИО и телефон)") + : fail("Форма не отрендерилась"); + + // ── 5. Экран замеров ────────────────────────────────────────────────────── + section("📐 Экран замеров"); + await page.evaluate(() => { location.hash = "#/measurements"; }); + await page.waitForTimeout(3000); + const jsErrMeasure = errors.flush(); + jsErrMeasure.length === 0 + ? pass("Нет JS-ошибок на экране замеров") + : fail("JS-ошибки на экране замеров", jsErrMeasure.slice(0, 2).join(" | ")); + + // ── 6. Экран сборок ─────────────────────────────────────────────────────── + section("🔧 Экран сборок"); + await page.evaluate(() => { location.hash = "#/assembly"; }); + await page.waitForTimeout(3000); + const jsErrAssembly = errors.flush(); + jsErrAssembly.length === 0 + ? pass("Нет JS-ошибок на экране сборок") + : fail("JS-ошибки на экране сборок", jsErrAssembly.slice(0, 2).join(" | ")); + + // ── 7. Снимок экрана для отчёта ─────────────────────────────────────────── + await page.evaluate(() => { location.hash = "#/clients"; }); + await page.waitForTimeout(2000); + await page.screenshot({ path: "tests/ui_last_run.png", fullPage: false }); + pass("Скриншот сохранён", "tests/ui_last_run.png"); + + await browser.close(); + printSummary(); +} + +function printSummary() { + const passed = RESULTS.filter(r => r.ok).length; + const failed = RESULTS.filter(r => !r.ok).length; + console.log(`\n${"=".repeat(55)}`); + console.log(` ИТОГО: ${passed} ✅ / ${failed} ❌`); + console.log(`${"=".repeat(55)}\n`); + if (failed > 0) { + console.log("📋 ЗАМЕЧАНИЯ К УСТРАНЕНИЮ:\n"); + RESULTS.filter(r => !r.ok).forEach(r => { + console.log(` ❌ ${r.name}`); + if (r.detail) console.log(` → ${r.detail}`); + }); + console.log(); + process.exit(1); + } else { + console.log("✅ Все UI-тесты прошли.\n"); + process.exit(0); + } +} + +run().catch(e => { + console.error("Критическая ошибка:", e.message); + process.exit(1); +});