Testo - extensible PHP testing framework. Tests without TestCase inheritance, middleware architecture, PSR-14 events, separate Assert/Expect facades.
docs/ # English (root)
├── index.md # Home page
├── docs/ # Documentation pages
└── .vitepress/
├── config.mts # Config: nav, sidebar, locales
└── theme/style.css # Custom styles (dough colors)
ru/ # Russian locale (same structure)
Tone: Informal but technically accurate. Code examples over explanations.
Home page rules:
- DON'T show code in features (text only, 1 sentence each)
- Don't mention competitors, use "old solutions"
Code examples:
- List of options first, then one code block with all examples (easier to read than many small blocks)
- Keep examples compact — show structure, not implementation (when implementation doesn't matter)
- Show contrast between approaches in examples
Text structure:
- Avoid tautology in lists, fix typos
- Small sections sometimes better integrated into existing ones
Markdown: Use ::: tip, ::: warning, ::: info, ::: question blocks
Adding pages:
- Create both
docs/page.md(EN) andru/docs/page.md(RU) - Add to sidebar in
.vitepress/config.mtsfor both locales - Internal links:
./pageor/docs/page(no.html)
Syncing translations:
- CRITICAL: When changing documentation content (adding sections, examples, explanations), ALWAYS update BOTH English and Russian versions
- This applies to content changes, NOT just translation quality fixes
- If you modify
docs/page.md, you MUST also updateru/docs/page.mdwith the translated version - Exception: Only fixing translation quality in
ru/doesn't require touching English version
Dead links: Create stub with ::: tip Coming Soon block
Styles: .vitepress/theme/style.css - brand colors --vp-c-brand-1, responsive breakpoints 960px/640px
Adding blog posts:
- Create both
blog/post.md(EN) andru/blog/post.md(RU) - Add link to
blog/index.mdandru/blog/index.md
Required frontmatter:
---
title: "Post Title"
date: 2025-01-01
description: "Short description for RSS and sharing"
image: /blog/post-name/preview.jpg
author: Author Name
---title,date,description— required for RSSimage— used for preview in blog list, og:image for social sharing, and displayed in post headerauthor— displayed in blog list and post header
Optional frontmatter:
outline— controls heading levels shown in the right sidebar (table of contents):outline: [2, 3]— show h2 and h3 (default behavior)outline: 'deep'— show all levels (h2-h6)outline: false— hide outline completelyoutline: 2— show only h2
Testo provides llms.txt for AI agents. Only docs/ pages are included — blog posts are excluded from llms generation entirely (no llms frontmatter in blog files).
Manifest: llms.config.ts — project-level metadata (title, summary, key facts, base URL). Edit when project description or key facts change.
Generator: .vitepress/llms.ts — builds llms.txt, llms-full.txt, and per-page .md files during buildEnd. Only scans docs/ directory.
Frontmatter fields (English docs/ pages only, not ru/ or blog/):
---
llms: true # default — included in "Docs" section
llms: "optional" # included in "Optional" section (secondary content)
llms: false # excluded from llms.txt
llms_description: "Technical description of what LLM learns from this page"
---llms— controls inclusion. Default istrue(can be omitted)."optional"for secondary content,falseto excludellms_description— short, technical description for LLM context. Lists key entities and concepts, not marketing text. Required for all included pages
Guidelines for llms_description:
- List specific classes, attributes, methods — not vague descriptions
- Example:
"#[BeforeTest], #[AfterTest], #[BeforeClass], #[AfterClass] lifecycle hooks, execution order, priority" - NOT:
"Learn about test lifecycle management"
When adding new doc pages: add llms_description to the English version frontmatter. Do NOT add llms frontmatter to blog posts.
Questions can be written anywhere in the article using ::: question blocks. At build time, they are extracted from their original positions and grouped into collapsible FAQ accordions.
Syntax:
::: question Can I run tests without config?
Yes, Testo looks for tests in the `tests` folder by default.
:::Frontmatter faqLevel controls where questions are rendered:
---
faqLevel: 1 # default — end of each h1 section (= end of page for most docs)
faqLevel: 2 # end of each h2 section
faqLevel: 0 # end of page (ignores headings)
faqLevel: false # no collection — questions stay in place as inline spoilers
---Plugin: .vitepress/faq.ts — markdown-it block rule + core rule, no external dependencies.
Styles: .vitepress/theme/style.css — .faq-section, .faq-item classes.
For documenting methods, functions, and PHP attributes in API reference pages. Renders a highlighted PHP signature box with description, parameters, and examples.
Plugin: .vitepress/func-block.ts — markdown-it block rule, Shiki highlighting.
Styles: .vitepress/theme/style.css — .func-block, .func-sig classes.
Full syntax (function):
<signature h="3" name="\Testo\Assert::same(mixed $actual, mixed $expected, string $message = ''): void">
<short>Checks strict equality of two values.</short>
<description>Uses `===` comparison. Unlike `equals()`, does not perform type coercion.</description>
<param name="$actual">The value being checked.</param>
<param name="$expected">The expected value.</param>
<example>
```php
Assert::same($user->role, 'admin');Attribute signature syntax:
<signature h="2" name="#[\Testo\Retry(int $maxAttempts = 3, bool $markFlaky = true)]">
<short>Declares a retry policy for a test on failure.</short>
</signature>Attribute signatures are detected by the #[ prefix in name. The #[...] wrapper is preserved in the rendered signature box. Headings display as #[ShortName] (e.g., #[Retry]). The inner FQN (without #[...]) is used for registry lookup and <attr> cross-references.
Attributes:
name(required) — full signature. For functions: FQN with types and return type (\Testo\Assert::method). For attributes:#[\Namespace\AttrName(params)]. Namespace is stripped for display.h— heading level for auto-generated heading ("3"→<h3>Assert::same</h3>or<h2>#[Retry]</h2>). Default:"0"(bold text, no heading).compact— compact rendering mode: signature + short + description inline, no card/sections. Good for simple methods in lists.
Inner tags (all optional):
<short>...</short>— one-liner rendered between heading and signature box. Super brief summary of the method.<description>...</description>— detailed method description, supports full markdown (paragraphs, lists, code blocks, etc.). Rendered below the signature box.<param name="$foo">...</param>— parameter description (inline markdown). Rendered under localized "Parameters:" / "Параметры:" label.<example>...</example>— full markdown block (code fences, text, etc.). Multiple allowed. Rendered under localized "Examples:" / "Примеры:" label.
Minimal usage (no description, no params):
<signature name="Assert::true(mixed $actual): void">
</signature>For cross-referencing methods inline within text. Renders as Shiki-highlighted code with a hover tooltip showing the full signature and description.
Plugin: .vitepress/func-block.ts — markdown-it inline rule. Registry: .vitepress/func-registry.ts — pre-scans all .md files at startup.
Syntax:
<func>\Testo\Assert::blank()</func>Renders as Assert::blank() with syntax highlighting. On hover, shows a tooltip with the full signature and <short> description from the corresponding <signature> block.
Behavior:
- If the referenced
<signature>hash > 0(navigable anchor), the reference is a clickable link - If
h="0"or noh, the reference shows tooltip only (no link) - If FQN is not found in any
<signature>block, renders as plain<code> - Locale-aware: EN pages reference EN signatures, RU pages reference RU signatures
Registry: All <signature> blocks with FQN names (starting with \) are collected at build startup. The <func> tag content is matched by stripping arguments: \Testo\Assert::blank() matches \Testo\Assert::blank(mixed $actual, string $message = ''): void.
For cross-referencing PHP attributes inline within text. Renders as #[ShortName] with Shiki highlighting and a hover tooltip showing the full signature and description.
Plugin: .vitepress/func-block.ts — markdown-it inline + block rules. Registry: .vitepress/func-registry.ts — separate attribute registry (parallel to function registry).
Syntax:
<attr>\Testo\Retry</attr>Takes the plain FQN (without #[...]) and renders as #[Retry] with syntax highlighting. On hover, shows a tooltip with the full attribute signature and <short> description from the corresponding <signature> block.
Behavior:
- Same linking/tooltip rules as
<func>: link ifh > 0, tooltip-only otherwise, plain<code>if not found - The
#[...]wrapping is added automatically — always pass plain FQN in the tag - Locale-aware: EN pages reference EN signatures, RU pages reference RU signatures
- Uses a separate registry from
<func>to avoid FQN collisions
For referencing PHP classes inline. Renders the short class name (without namespace) with a hover tooltip showing the full FQN.
Plugin: .vitepress/func-block.ts — inline + block rules.
Syntax:
<class>\Testo\Assert</class>Renders as Assert styled as inline code. On hover, shows a tooltip with the full class name \Testo\Assert.
Behavior:
- Works both inline (inside text) and at the start of a line (block rule wraps in paragraph)
- Namespace is stripped for display:
\Testo\Assert\AssertPlugin→AssertPlugin - Styled as inline
<code>withvar(--vp-code-color)andvar(--vp-code-bg)
Styles: .vitepress/theme/style.css — .class-ref class.
For linking to plugin documentation pages by name. Renders as a clickable link to the plugin's page.
Plugin: .vitepress/func-block.ts — inline + block rules. Registry: .vitepress/plugin-block.ts — collects <plugin-info> blocks.
Syntax:
<plugin>Assert</plugin>Renders as a link to the Assert plugin page. The name is matched case-insensitively against <plugin-info> blocks.
Behavior:
- Lookup by
nameattribute from<plugin-info>tags (case-insensitive) - If no match found, renders as plain text
- Locale-aware: EN pages link to EN plugin pages, RU to RU
Block-level tag for plugin documentation pages. Renders a styled info card with plugin class, inclusion status, and optional links. Also registers the plugin in the registry for <plugin> cross-references.
Plugin: .vitepress/plugin-block.ts — markdown-it block rule + registry with pre-scan.
Syntax:
<plugin-info class="\Testo\Assert\AssertPlugin" name="Assert" included="\Testo\Application\Config\Plugin\SuitePlugins" />
<plugin-info class="\Testo\Convention\ConventionPlugin" name="Convention" />
<plugin-info class="\Testo\Filter\FilterPlugin" name="Filter" included="\Testo\Application\Config\Plugin\ApplicationPlugins" github="https://..." />Attributes:
class(required) — FQN of the plugin class. Rendered as<class>with tooltip.name(required) — human-readable plugin name. Used for<plugin>cross-references.included— FQN of the plugin set (e.g.\Testo\Application\Config\Plugin\SuitePlugins). Rendered as<class>with tooltip. Omit if the plugin must be registered manually.github— URL to plugin's GitHub page (optional).
npm run docs:dev # Dev server at localhost:5173
npm run docs:build # Build to .vitepress/dist/
npm run docs:preview # Preview buildFile: .vitepress/config.mts
locales: root (EN) +ru(RU) with separate nav/sidebarcleanUrls: true,lastUpdated: true,search.provider: 'local'- Nav and Sidebar defined in
themeConfigfor each locale - Sidebar sections: Introduction, Guide, Customization
-
Естественность важнее дословности
- Перевод должен звучать так, как говорят русские разработчики
- Избегать кальки с английского, если это звучит неестественно
-
Сохранять технические термины
- Не переводить названия классов, методов, атрибутов, пакетов
- Оставлять код примеры без изменений
| Английский | ❌ Неправильно | ✅ Правильно | Примечание |
|---|---|---|---|
| test | — | тест | Один метод теста или InlineTest |
| test case | тестовый случай | Test Case / тест-кейс / тестовый класс / класс тестов / набор тестов | Класс/файл с тестами. Выбор зависит от контекста |
| test suite | тестовый набор | Test Suite / комплект тестов | Глобальная группа (Unit, Feature). Предпочтительно без перевода |
| data provider | поставщик данных | провайдер данных | |
| dataset | — | датасет / набор данных | Оба допустимы |
| callable | вызываемый | вызываемый объект | В контексте |
| closure | закрытие | замыкание |
Важно понимать правильную иерархию компонентов:
- Test Suite (комплект тестов) - Unit, Integration, Feature
- События:
TestSuite*(TestSuiteStarting, TestSuiteFinished)
- События:
- Test Case (тестовый класс) - UserTest, OrderTest
- События:
TestCase*(TestCaseStarting, TestCaseFinished)
- События:
- Test (тестовый метод или функция) - testLogin(), testCreate()
- События:
Test*(TestStarting, TestFinished, TestBatchStarting)
- События:
| Английский | ❌ Неправильно | ✅ Правильно |
|---|---|---|
| Simple as that | Просто как это | Вот так просто |
| right on the method | прямо на методе | непосредственно над методом |
-
Избегать канцеляризмов
- Вместо "тестовый случай" → "тест"
- Вместо "осуществить проверку" → "проверить"
-
Сохранять тон оригинала
- Если оригинал неформальный и дружелюбный — перевод тоже
- Технические термины оставлять точными
-
Порядок слов
- Использовать естественный русский порядок слов
- Не копировать английскую структуру предложения
-
Термин + конкретное значение
- При упоминании технического термина с конкретным значением использовать дефис для разделения
- Примеры:
- ✅ "Test Suite - Unit" (не "Test Suite Unit")
- ✅ "комплект тестов Unit" (альтернатива)
- ✅ "провайдер данных - UserDataProvider"
Перед финализацией перевода задать вопросы:
- Так ли говорят русские разработчики?
- Звучит ли фраза естественно при чтении вслух?
- Сохранен ли технический смысл оригинала?