zapretgui/tests/test_onboarding_tour_drift.py
loop-uh 46d333bddc
All checks were successful
Zapret source guards / validate (push) Successful in 13s
Windows-сборка / Dev Dev 21.1.7.78 опубликован
Экскурсия проверяется целиком: каждый шаг подсвечивает именно тот блок, о котором говорит
- общий прогон идёт по всем 102 шагам на настоящих страницах; пресеты,
  профили, стратегии и hosts получают пример данных через обычный вход
  страницы;
- для каждого шага проверяется: подсветка обводит ровно отданный блок,
  блок ничем не закрыт, карточка подсказки его не заслоняет, а названия
  из текста шага написаны в подсвеченном блоке целой надписью;
- сверка названий со строками программы ищет название целиком, а не
  куском другого слова;
- исправлен шаг про список сайтов профиля: поля называются «Ваши
  записи» и «Встроенные записи (только просмотр)», а не «База».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:12:35 +03:00

149 lines
7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Экскурсия не расходится с программой.
Эти проверки падают, когда программу поменяли, а экскурсию «Как пользоваться
программой» — нет: появилась страница, о которой она молчит, или кнопку
переименовали, а текст шага зовёт её по-старому. Что делать в каждом случае,
написано в сообщении об ошибке и в навыке
``.codex/skills/zapretgui-guided-tour/SKILL.md``.
"""
from __future__ import annotations
import ast
import re
import unittest
from pathlib import Path
from app.page_names import PageName
SKILL_HINT = "Как править экскурсию: .codex/skills/zapretgui-guided-tour/SKILL.md"
SRC_ROOT = Path(__file__).resolve().parents[1] / "src"
# В «ёлочках» экскурсия цитирует названия кнопок, вкладок и страниц. Здесь — то,
# что названием не является: примеры составных надписей с числами, технические
# примеры и чужие тексты. Название из программы сюда не добавлять — исправить
# текст шага, чтобы он звал вещь её настоящим именем.
QUOTES_NOT_FROM_THE_PROGRAM = frozenset(
{
# примеры надписей, которые программа собирает из чисел и названий
"на этом сервисе в 13 пресетах",
"на 28 сервисах",
"ещё 2",
"3 из 5",
"Снова открываются: Discord",
# технические примеры
"multisplit seqovl700",
"you#tube.com",
"you",
"tube.com",
# надписи на чужих сайтах
"недоступно в вашей стране",
"not available in your region",
# пояснения и образные слова
"имя сайта → адрес",
"стучится",
"испорчен",
"срочный",
"до",
"после",
}
)
_QUOTE = re.compile(r"«([^«»]+)»")
def _program_strings() -> str:
"""Все строки программы, кроме самой экскурсии: тексты интерфейса и строки из кода."""
from app.ui_texts import TEXTS
parts = [
str(values.get("ru") or "")
for key, values in TEXTS.items()
# Кнопки самой карточки («Далее», «Назад») — тоже часть программы.
if not key.startswith("onboarding.step.")
]
for path in sorted(SRC_ROOT.rglob("*.py")):
# Тексты интерфейса уже взяты выше, а в коде экскурсии лежат её же примеры.
if path.name.startswith("ui_texts") or "onboarding" in path.relative_to(SRC_ROOT).as_posix():
continue
try:
tree = ast.parse(path.read_text(encoding="utf-8-sig"))
except SyntaxError:
continue
parts.extend(
node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)
)
return "\n".join(parts)
class TourCoversEveryPageTests(unittest.TestCase):
def test_every_page_of_the_program_is_in_the_tour_or_explicitly_left_out(self) -> None:
from ui.onboarding.steps import COMMON_TOUR_PAGES, MODE_TOUR_PAGES, TOUR_PAGES_WITHOUT_STEPS
in_tour = set(COMMON_TOUR_PAGES.values())
for mode_pages in MODE_TOUR_PAGES.values():
in_tour.update(mode_pages.values())
forgotten = sorted(page.name for page in set(PageName) - in_tour - set(TOUR_PAGES_WITHOUT_STEPS))
self.assertEqual(
forgotten,
[],
"У программы появилась страница, о которой экскурсия не знает. Добавьте для неё шаги "
"в ui/onboarding/steps.py или запишите в TOUR_PAGES_WITHOUT_STEPS, почему они не нужны. "
+ SKILL_HINT,
)
both = sorted(page.name for page in in_tour & set(TOUR_PAGES_WITHOUT_STEPS))
self.assertEqual(both, [], "Страница уже есть в экскурсии — уберите её из TOUR_PAGES_WITHOUT_STEPS.")
for page, reason in TOUR_PAGES_WITHOUT_STEPS.items():
self.assertTrue(reason.strip(), page.name)
def test_every_page_listed_for_the_tour_has_at_least_one_step(self) -> None:
from ui.onboarding.steps import COMMON_TOUR_PAGES, MODE_TOUR_PAGES, TOUR_STEPS
listed = set(COMMON_TOUR_PAGES)
for mode_pages in MODE_TOUR_PAGES.values():
listed.update(mode_pages)
visited = {step.page for step in TOUR_STEPS if step.page}
self.assertEqual(
sorted(listed - visited),
[],
"Страница записана в таблицу экскурсии, но ни один шаг её не открывает. " + SKILL_HINT,
)
class TourTextsNameRealThingsTests(unittest.TestCase):
def test_names_quoted_in_tour_texts_exist_in_the_program(self) -> None:
from app.ui_texts import TEXTS
program = _program_strings()
unknown: dict[str, list[str]] = {}
used_exceptions: set[str] = set()
for key, values in TEXTS.items():
if not key.startswith("onboarding.step."):
continue
for quote in _QUOTE.findall(str(values.get("ru") or "")):
if "{" in quote:
# Живое значение: его подставляет сама страница.
continue
if quote in QUOTES_NOT_FROM_THE_PROGRAM:
used_exceptions.add(quote)
continue
# Название целиком, а не кусок другого слова: «База» не должна находиться в «базах».
if re.search(rf"(?<!\w){re.escape(quote)}(?!\w)", program) is None:
unknown.setdefault(quote, []).append(key)
self.assertEqual(
unknown,
{},
"Экскурсия называет то, чего в программе с таким именем нет: кнопку или вкладку переименовали "
"или убрали. Поправьте текст шага (ru и en), а если шаг больше не нужен — удалите его. "
+ SKILL_HINT,
)
# Список исключений не копит мусор после правки текстов.
self.assertEqual(sorted(QUOTES_NOT_FROM_THE_PROGRAM - used_exceptions), [])
if __name__ == "__main__":
unittest.main()