TYPO3 Extension Testing
Skill: typo3-extension-testing
Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.
Markdown source#
---
name: typo3-extension-testing
description: 'Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.'
compatibility: Needs the typo3-dev-companion MCP server, which owns every lookup this workflow routes to and publishes this skill together with the references/base.md it opens on. Install it from github.com/TYPO3/dev-companion and run typo3-dev-companion install in the project. A copy taken out of that repository's skills directory alone has neither the tools nor that base file.
---
# TYPO3 Extension Testing
Establish or grow the smallest useful test and static-quality surface at the
correct layer. Make the first green run part of setup, and run only commands
supported by the checkout. Keep this skill as routing and workflow; never retain
version-specific APIs, paths, dependency constraints, or commands that the
installation or checkout owns.
## Establish the test surface
Work through [references/base.md](references/base.md) first — it fixes the order
every task here starts in and why that order is not interchangeable. Two of its
answers decide this workflow before any test is written: the commands
`typo3_project_describe` reports are the only ones that exist in this
repository, and the layers `typo3_extension_describe` reports below `Tests/` are
what the extension has today — an empty list is the answer that there is no
harness yet.
Then, for this workflow:
- `typo3_documentation_lookup` with several short English queries and the target
TYPO3 version when dependency setup, bootstrapping, fixtures, browser
configuration, or an API needs confirmation.
- Verify that the harness for every relevant layer can discover and run its
tests. Treat missing or broken infrastructure as a prerequisite of the
requested work, not as a separate kind of task and not as a reason to force
the behavior into another layer.
- Read the target code, existing tests and fixtures, test configuration,
dependency manifests, CI, and the development environment.
If a lookup is unavailable, state the gap separately from a lookup that found no
match. Do not replace current project evidence with remembered TYPO3 setup.
## Choose the layer and its owner
Read [references/checklist.md](references/checklist.md) when selecting layers,
establishing missing infrastructure, choosing commands, or auditing coverage.
After selecting a layer, read only its implementation guide:
- [references/phpunit.md](references/phpunit.md) for unit or functional tests.
- [references/playwright.md](references/playwright.md) for browser,
accessibility, or visual tests.
- [references/static-quality.md](references/static-quality.md) for static
analysis, coding standards, and the commands that run them.
- Prefer a unit test for isolated logic without TYPO3 state or persistence.
- Use a functional test when TYPO3 bootstrapping, configuration, database
schema, DataHandler, repositories, services, or integration between framework
components is part of the behavior.
- Use a browser test for rendered user journeys, backend interaction,
JavaScript, or accessibility behavior that cannot be established below the UI.
- Use static analysis and coding standards for the defects and the style no test
observes. A task that asks for them establishes them whether or not the
project already runs them; a task that does not ask extends what is there and
reports what is missing rather than introducing a check nobody requested.
- Keep unit and functional infrastructure with the extension whose PHP it
exercises. Keep browser infrastructure with the runnable project, because it
needs a served site rather than an extension package alone.
- Establish only the layers the task can justify. A setup request does not
require every possible test runner.
## Establish or repair the required harness
Before adding or extending coverage, fix any missing or broken prerequisite for
the selected layer. For an explicit setup request, this is the requested work;
for a review-only request, report the defect without changing it.
1. Determine compatible development dependencies from the project's constraints,
installed packages, Composer resolution, and versioned documentation. Add a
dependency only when changes are in scope and the selected layer requires it;
never guess its version.
2. Take configuration and bootstrap templates from the installed dependency or
the source named by `typo3_hint_lookup`. Copy and adapt templates that say
they are examples; do not point extension suites into a core checkout.
3. Preserve working configuration, scripts, and CI. Extend them instead of
creating a parallel harness.
4. Give each selected layer one stable local command before adding CI. Derive
functional database settings and browser URLs from the project's environment;
do not commit credentials or machine-specific hosts.
5. For unit or functional tests, establish the suite configuration, bootstrap,
test directories, extension loading, and environment the returned guidance
requires. Never translate a core-only `runTests.sh` command into an extension
command.
6. For browser tests, require a runnable site and establish project-owned runner
configuration, scripts, artifacts, and one real target. Choose host,
container, or dedicated browser image from the project rather than imposing
one topology.
7. For static analysis and coding standards, establish one project-owned command
per check and keep the command that reports apart from the one that writes.
Fix a new finding rather than recording it in a baseline, and keep automatic
formatting inside the first-party paths the project intends it to touch.
8. Make CI call the same commands that passed locally. Add a version matrix only
for combinations the package declares and the dependency solver accepts.
## Add or extend tests
- Follow nearby passing tests and the established harness. If the required layer
is missing, establish it first instead of forcing the behavior into a cheaper
layer.
- Preserve a regression test that fails for the observed defect before applying
its fix when practical.
- Keep fixtures minimal and deterministic. Avoid unrelated site data, execution
order, wall-clock timing, and external services.
- Put reusable setup at the narrowest scope that removes meaningful duplication.
- Test observable behavior and public contracts; avoid assertions tied only to
implementation details.
- Distinguish a broken runner, a missing environment prerequisite, and a failing
assertion before changing production code.
## Prove the result
1. Prove setup with a meaningful test at every layer established by the task. Do
not add `assertTrue(true)` or production code whose only purpose is to give
the harness something to test. If no unit-testable behavior exists, prove
discovery and report the unit suite as empty.
2. Run the narrowest relevant test first, then its containing local suite.
3. Run the declared CI-equivalent commands after the local commands pass.
4. For browser work, execute at least one real spec and confirm its expected
artifact or report is produced.
5. For a static check, run it again after its fix command and inspect the
working tree for files the fixer touched outside the intended scope.
6. Report the exact commands run, results, files added or changed, and checks
not run with the reason.
7. Draft the message for each commit with `typo3_commit_message_guide` and
`workflow="project"`.
[references/static-quality.md](references/static-quality.md) says where a
formatting pass is split off and in which order the commits go; what each of
them says is this tool's answer.
This skill owns testing and static-quality infrastructure, the changes they
require, and the execution of both. A broad conformance audit is
`typo3-extension-conformance`, a documentation rewrite is
`typo3-extension-documentation`, a backend module is
`typo3-backend-module-development` and a content element is
`typo3-content-element-development`. Hand that work to its owner at the verified
point, stop before editing its files, and retain only the testing part.
markdown
---
name: typo3-extension-testing
description: 'Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.'
compatibility: Needs the typo3-dev-companion MCP server, which owns every lookup this workflow routes to and publishes this skill together with the references/base.md it opens on. Install it from github.com/TYPO3/dev-companion and run typo3-dev-companion install in the project. A copy taken out of that repository's skills directory alone has neither the tools nor that base file.
---
# TYPO3 Extension Testing
Establish or grow the smallest useful test and static-quality surface at the
correct layer. Make the first green run part of setup, and run only commands
supported by the checkout. Keep this skill as routing and workflow; never retain
version-specific APIs, paths, dependency constraints, or commands that the
installation or checkout owns.
## Establish the test surface
Work through [references/base.md](references/base.md) first — it fixes the order
every task here starts in and why that order is not interchangeable. Two of its
answers decide this workflow before any test is written: the commands
`typo3_project_describe` reports are the only ones that exist in this
repository, and the layers `typo3_extension_describe` reports below `Tests/` are
what the extension has today — an empty list is the answer that there is no
harness yet.
Then, for this workflow:
- `typo3_documentation_lookup` with several short English queries and the target
TYPO3 version when dependency setup, bootstrapping, fixtures, browser
configuration, or an API needs confirmation.
- Verify that the harness for every relevant layer can discover and run its
tests. Treat missing or broken infrastructure as a prerequisite of the
requested work, not as a separate kind of task and not as a reason to force
the behavior into another layer.
- Read the target code, existing tests and fixtures, test configuration,
dependency manifests, CI, and the development environment.
If a lookup is unavailable, state the gap separately from a lookup that found no
match. Do not replace current project evidence with remembered TYPO3 setup.
## Choose the layer and its owner
Read [references/checklist.md](references/checklist.md) when selecting layers,
establishing missing infrastructure, choosing commands, or auditing coverage.
After selecting a layer, read only its implementation guide:
- [references/phpunit.md](references/phpunit.md) for unit or functional tests.
- [references/playwright.md](references/playwright.md) for browser,
accessibility, or visual tests.
- [references/static-quality.md](references/static-quality.md) for static
analysis, coding standards, and the commands that run them.
- Prefer a unit test for isolated logic without TYPO3 state or persistence.
- Use a functional test when TYPO3 bootstrapping, configuration, database
schema, DataHandler, repositories, services, or integration between framework
components is part of the behavior.
- Use a browser test for rendered user journeys, backend interaction,
JavaScript, or accessibility behavior that cannot be established below the UI.
- Use static analysis and coding standards for the defects and the style no test
observes. A task that asks for them establishes them whether or not the
project already runs them; a task that does not ask extends what is there and
reports what is missing rather than introducing a check nobody requested.
- Keep unit and functional infrastructure with the extension whose PHP it
exercises. Keep browser infrastructure with the runnable project, because it
needs a served site rather than an extension package alone.
- Establish only the layers the task can justify. A setup request does not
require every possible test runner.
## Establish or repair the required harness
Before adding or extending coverage, fix any missing or broken prerequisite for
the selected layer. For an explicit setup request, this is the requested work;
for a review-only request, report the defect without changing it.
1. Determine compatible development dependencies from the project's constraints,
installed packages, Composer resolution, and versioned documentation. Add a
dependency only when changes are in scope and the selected layer requires it;
never guess its version.
2. Take configuration and bootstrap templates from the installed dependency or
the source named by `typo3_hint_lookup`. Copy and adapt templates that say
they are examples; do not point extension suites into a core checkout.
3. Preserve working configuration, scripts, and CI. Extend them instead of
creating a parallel harness.
4. Give each selected layer one stable local command before adding CI. Derive
functional database settings and browser URLs from the project's environment;
do not commit credentials or machine-specific hosts.
5. For unit or functional tests, establish the suite configuration, bootstrap,
test directories, extension loading, and environment the returned guidance
requires. Never translate a core-only `runTests.sh` command into an extension
command.
6. For browser tests, require a runnable site and establish project-owned runner
configuration, scripts, artifacts, and one real target. Choose host,
container, or dedicated browser image from the project rather than imposing
one topology.
7. For static analysis and coding standards, establish one project-owned command
per check and keep the command that reports apart from the one that writes.
Fix a new finding rather than recording it in a baseline, and keep automatic
formatting inside the first-party paths the project intends it to touch.
8. Make CI call the same commands that passed locally. Add a version matrix only
for combinations the package declares and the dependency solver accepts.
## Add or extend tests
- Follow nearby passing tests and the established harness. If the required layer
is missing, establish it first instead of forcing the behavior into a cheaper
layer.
- Preserve a regression test that fails for the observed defect before applying
its fix when practical.
- Keep fixtures minimal and deterministic. Avoid unrelated site data, execution
order, wall-clock timing, and external services.
- Put reusable setup at the narrowest scope that removes meaningful duplication.
- Test observable behavior and public contracts; avoid assertions tied only to
implementation details.
- Distinguish a broken runner, a missing environment prerequisite, and a failing
assertion before changing production code.
## Prove the result
1. Prove setup with a meaningful test at every layer established by the task. Do
not add `assertTrue(true)` or production code whose only purpose is to give
the harness something to test. If no unit-testable behavior exists, prove
discovery and report the unit suite as empty.
2. Run the narrowest relevant test first, then its containing local suite.
3. Run the declared CI-equivalent commands after the local commands pass.
4. For browser work, execute at least one real spec and confirm its expected
artifact or report is produced.
5. For a static check, run it again after its fix command and inspect the
working tree for files the fixer touched outside the intended scope.
6. Report the exact commands run, results, files added or changed, and checks
not run with the reason.
7. Draft the message for each commit with `typo3_commit_message_guide` and
`workflow="project"`.
[references/static-quality.md](references/static-quality.md) says where a
formatting pass is split off and in which order the commits go; what each of
them says is this tool's answer.
This skill owns testing and static-quality infrastructure, the changes they
require, and the execution of both. A broad conformance audit is
`typo3-extension-conformance`, a documentation rewrite is
`typo3-extension-documentation`, a backend module is
`typo3-backend-module-development` and a content element is
`typo3-content-element-development`. Hand that work to its owner at the verified
point, stop before editing its files, and retain only the testing part.
References#
Where every task starts#
# Where every task starts
## Nothing starts until the server answers
A skill is a file the installer left behind: it loads and reads the same whether
the tools behind it are connected or not, and neither side notices. So the first
call below is also the check.
- No `typo3_` tool in this session, or a first call that errors: stop, say this
workflow needs the server and it is not there, and name what came back.
- Do not fall back to general TYPO3 knowledge or start reading the checkout.
That answer carries this workflow's order and confidence and none of its
evidence, and nothing in it says which of the two it is.
- Continue only when asked to after saying so, and repeat it in the answer and
in every finding a lookup would have carried.
## The order
This is the order, and it is an order rather than a list because each step
decides what the next one is worth. A convention fetched after the code has been
read confirms a view instead of testing it. A command recommended before the
project's own are known is a guess that sounds like advice. Where a step below
carries a condition for skipping it, that condition is narrow on purpose: a
prescription that gets skipped teaches the next reader to skip the ones that
matter too.
1. **`typo3_project_describe`** — the installation, its TYPO3 and PHP version,
the extensions that are the project's own, its sites, and the commands this
repository actually declares. Every later answer is filtered by that version,
and a check the repository does not declare is a wrong answer however
sensible it sounds. It ends with the whole procedures this server carries, as
ids: that list is the only place they are named to a client that renders no
resource list, and each one is a `typo3_rule_lookup` with that `documentId`
rather than a search.
2. **`typo3_extension_describe`** for each extension in scope — what it
registers, and what it ships beside that: its manual, its README, its test
layers, its XLF files with the source language each one declares. What it
does *not* ship is answered too, and that is the half no file listing can
give you.
Where step 1 reported none — a core checkout is the case, since it names the
project's own extensions and not TYPO3's — that answer is this step, and
there is nothing to call. Say so. A step passed over in silence cannot be
told from one that was dropped.
3. **`typo3_task_guide`** with a short English task, the paths it touches, the
target version and the change type — the workflow this task belongs to and
the checks that come with it.
Run it in every session, this skill's own tasks included. The brief is built
from the paths as well as the task text, and no skill knows which paths the
caller is holding: a skill that covers the task is not that brief, and
skipping the step costs the hints and the core checks those paths match.
Where the guide's own answer is what named this skill, this is one call for
an answer already in the session. That is the price of a step there is
nothing to decide about.
4. **`typo3_hint_lookup`** for each subsystem in scope, with its concrete paths.
One query per subsystem; a single broad query is not subsystem evidence.
Where step 3 ran with those paths, its answer says whether this step is still
owed. A brief that carried everything the lookup matched says so — "these are
everything typo3_hint_lookup matches for these paths" — and there the call is
made and asking again returns the same hints. One that stopped short says
that instead and names the ids it left, and those are what is owed: fetch
them by id rather than repeating the query. Read the sentence rather than the
populated `hints` key, which is present either way and does not tell the two
apart.
5. **`typo3_changelog_lookup` with `type: deprecation`**, at each major the
package declares, bounded by `tag` and with the query omitted. Those three
are the changelog's own axes, and the extension's vocabulary is not among
them: an entry carries a query only when its title carries every word of it
at once, and the core titled those entries about its own code. Words taken
from what step 2 reported are therefore matched against titles they were
never written in, and the sweep comes back empty however right the query
looks.
Step 2 picks the tags instead. `ext:core`, `ext:frontend`, `ext:form` and the
rest name the system extension a change is **in** — one call for each one the
package requires, renders through or registers into, which is more than its
manifest lists — and `TCA`, `TypoScript`, `Fluid`, `YAML`, `Backend`,
`Frontend` name the surface, one for each kind of file it ships. An extension
key of your own is not among them and matches nothing. Every call also
returns every tag that version and type carry, so the second call onwards is
read off the first rather than guessed at.
Step 2 is what the answers are checked against, which is the other half the
words were doing. Verify each identifier that comes back in the checkout — a
deprecation nothing here calls is not a finding — and carry the
`FullyScanned` / `PartiallyScanned` tag into the answer, because it says
whether the Extension Scanner can find the remaining call sites or whether
that reading is yours. Bounded this way the sweep is still writable before a
file is opened, which is why it is a step of the order: one left to the
reading reaches only what a finding stumbled into, and the deprecation that
decides whether the package survives the next major is not usually the one a
finding walks past.
A changelog records change events, so a pattern nothing has touched for ten
majors has no entry at all. An empty sweep is therefore not an answer about
what still works. "Does this still work in version N" goes to
`typo3_documentation_lookup` at that version — here, and whenever the reading
raises it again.
That is a question for a documented surface — a ViewHelper, a TCA type, a
TypoScript setting. The manual matches page titles and section paths, never
the text of a page, so a PHP identifier has no page to be titled after. It
reaches whatever its own words happen to spell instead. An identifier goes to
`typo3_changelog_lookup` under its own name, which reaches the entries
writing it however the change was titled, and then to the class below. Where
the manual has no page for a surface either, that is a result and not an
answer. Undocumented is not unsupported.
A task that produces no change does not reach this step at all. The property
is what the task produces, and a triage, a reproduction and a review are
illustrations of it rather than the list it is read off. The sweep asks what
a package will have to stop calling, and a task that writes nothing is not
going to call anything. Followed literally in a triage of one issue it costs
one call per declared major per tag and none of them bears on the report.
The exemption ends where the workflow produces a change. A review asked to
make the change is that other workflow, and it starts this order again
holding the files it is about to write.
Skip the sweep only where the change touches no TYPO3 API — a code style
fixer, a CI file, an `.editorconfig`. A deprecation is a statement about API
the package calls, so a change that calls none has nothing for the sweep to
land on and it is empty before it is run. That condition is worth stating
because this is the most expensive step of the order: one call per declared
major per tag. Which side a change falls on is read off the files it touches
and never off the task it started as — one PHP file edited along the way puts
it back among the ordinary ones, and a skip there costs the deprecation no
finding would have walked into.
A report names the step it did not reach, under either exemption. A step
passed over in silence cannot be told from one that was dropped, which is
what step 2 already asks of itself.
**Then** read the checkout. Not before: listing the files first makes everything
after the listing look optional, and the conventions arrive as a footnote to a
verdict that has already formed.
## When the lookups run out
A behaviour question that survives the lookups above is read out of the
installed source rather than guessed at. What answers it is the class that
implements the behaviour and the one it inherits from. That reading is the step
after the lookups, and what it replaces is changing the code until it works.
What it settles is what this installation does and never what TYPO3 supports. So
a finding says the question could not be settled beyond the version installed,
and an answer built on the reading names the version it holds for.
## Two kinds of lookup, and neither stands in for the other
`typo3_backend_module_lookup`, `typo3_icon_lookup`, `typo3_label_lookup`,
`typo3_fluid_namespace_list` and `typo3_configuration_lookup` report what is
registered, what a path resolves to, what a value really is at runtime. They
establish the facts of this installation and they are never a verdict on it.
`typo3_hint_lookup` and `typo3_documentation_lookup` say whether those facts are
right. A subsystem confirmed by its own runtime lookup can still break every
rule that governs it, so it is not established until both were asked.
## A rule is read in both directions
It says what new code should do, and it says what this checkout is already doing
wrong. A file that has settled into the opposite of a rule is a finding, not a
local style to preserve: consistency with a project's own habit establishes
nothing about whether the habit is right.
## What the code is for is evidence, and the repository states it
A mechanism that costs something is not a defect for costing it. Before
reporting one, find what it is there for — the manual, the README, the
changelog, the setting it is driven by, the versions the package declares it
supports — and say so. Where a purpose is documented, what you have is a
trade-off to name with its cost and its alternative, not a defect; where you
cannot find one, the finding says that it could not be established rather than
that none exists. This is the other direction of the rule above, and skipping it
turns a review into a list of everything the author did on purpose.
## What a finding rests on is part of the finding
Three things carry one: a file that was read, at its path and its line; a
command that was run, with what it printed; a mechanism traced into an installed
package. Say which of the three it is. Leaving it unsaid gives a finding read
out of a CI file the weight of one with a verified line, and the reader has no
way to separate them again.
Where one of the project's own commands would settle it, run it.
`typo3_project_describe` marks each command it lists **check**, **change** or
**unknown**, read off the declared body: a check reports and hands the code back
as it was, so even a task told not to change files runs it, and the linter the
repository already declares is the cheapest evidence in it. A change is not run
under that instruction, and an unknown — a test suite, a shell pipeline, a
console command — is named in the answer as evidence that is available rather
than run unasked. What a check prints is not the finding: the configuration that
makes it fail is still what the finding is about, and the run is what takes that
finding from derived to established.
## What this server does not know
It does not read your working tree. Which files changed, which branch you are
on, and whether a path or an identifier still exists there are yours to
establish — then pass the concrete paths back, because that is what turns a
general convention into an answer about this code.
## Query it in English
The knowledge is written in English and matched lexically, so a query in another
language reaches the loanwords the two happen to share and nothing else.
Translate the subject before calling and the answer back afterwards, whatever
language you are speaking with the user.
markdown
# Where every task starts
## Nothing starts until the server answers
A skill is a file the installer left behind: it loads and reads the same whether
the tools behind it are connected or not, and neither side notices. So the first
call below is also the check.
- No `typo3_` tool in this session, or a first call that errors: stop, say this
workflow needs the server and it is not there, and name what came back.
- Do not fall back to general TYPO3 knowledge or start reading the checkout.
That answer carries this workflow's order and confidence and none of its
evidence, and nothing in it says which of the two it is.
- Continue only when asked to after saying so, and repeat it in the answer and
in every finding a lookup would have carried.
## The order
This is the order, and it is an order rather than a list because each step
decides what the next one is worth. A convention fetched after the code has been
read confirms a view instead of testing it. A command recommended before the
project's own are known is a guess that sounds like advice. Where a step below
carries a condition for skipping it, that condition is narrow on purpose: a
prescription that gets skipped teaches the next reader to skip the ones that
matter too.
1. **`typo3_project_describe`** — the installation, its TYPO3 and PHP version,
the extensions that are the project's own, its sites, and the commands this
repository actually declares. Every later answer is filtered by that version,
and a check the repository does not declare is a wrong answer however
sensible it sounds. It ends with the whole procedures this server carries, as
ids: that list is the only place they are named to a client that renders no
resource list, and each one is a `typo3_rule_lookup` with that `documentId`
rather than a search.
2. **`typo3_extension_describe`** for each extension in scope — what it
registers, and what it ships beside that: its manual, its README, its test
layers, its XLF files with the source language each one declares. What it
does *not* ship is answered too, and that is the half no file listing can
give you.
Where step 1 reported none — a core checkout is the case, since it names the
project's own extensions and not TYPO3's — that answer is this step, and
there is nothing to call. Say so. A step passed over in silence cannot be
told from one that was dropped.
3. **`typo3_task_guide`** with a short English task, the paths it touches, the
target version and the change type — the workflow this task belongs to and
the checks that come with it.
Run it in every session, this skill's own tasks included. The brief is built
from the paths as well as the task text, and no skill knows which paths the
caller is holding: a skill that covers the task is not that brief, and
skipping the step costs the hints and the core checks those paths match.
Where the guide's own answer is what named this skill, this is one call for
an answer already in the session. That is the price of a step there is
nothing to decide about.
4. **`typo3_hint_lookup`** for each subsystem in scope, with its concrete paths.
One query per subsystem; a single broad query is not subsystem evidence.
Where step 3 ran with those paths, its answer says whether this step is still
owed. A brief that carried everything the lookup matched says so — "these are
everything typo3_hint_lookup matches for these paths" — and there the call is
made and asking again returns the same hints. One that stopped short says
that instead and names the ids it left, and those are what is owed: fetch
them by id rather than repeating the query. Read the sentence rather than the
populated `hints` key, which is present either way and does not tell the two
apart.
5. **`typo3_changelog_lookup` with `type: deprecation`**, at each major the
package declares, bounded by `tag` and with the query omitted. Those three
are the changelog's own axes, and the extension's vocabulary is not among
them: an entry carries a query only when its title carries every word of it
at once, and the core titled those entries about its own code. Words taken
from what step 2 reported are therefore matched against titles they were
never written in, and the sweep comes back empty however right the query
looks.
Step 2 picks the tags instead. `ext:core`, `ext:frontend`, `ext:form` and the
rest name the system extension a change is **in** — one call for each one the
package requires, renders through or registers into, which is more than its
manifest lists — and `TCA`, `TypoScript`, `Fluid`, `YAML`, `Backend`,
`Frontend` name the surface, one for each kind of file it ships. An extension
key of your own is not among them and matches nothing. Every call also
returns every tag that version and type carry, so the second call onwards is
read off the first rather than guessed at.
Step 2 is what the answers are checked against, which is the other half the
words were doing. Verify each identifier that comes back in the checkout — a
deprecation nothing here calls is not a finding — and carry the
`FullyScanned` / `PartiallyScanned` tag into the answer, because it says
whether the Extension Scanner can find the remaining call sites or whether
that reading is yours. Bounded this way the sweep is still writable before a
file is opened, which is why it is a step of the order: one left to the
reading reaches only what a finding stumbled into, and the deprecation that
decides whether the package survives the next major is not usually the one a
finding walks past.
A changelog records change events, so a pattern nothing has touched for ten
majors has no entry at all. An empty sweep is therefore not an answer about
what still works. "Does this still work in version N" goes to
`typo3_documentation_lookup` at that version — here, and whenever the reading
raises it again.
That is a question for a documented surface — a ViewHelper, a TCA type, a
TypoScript setting. The manual matches page titles and section paths, never
the text of a page, so a PHP identifier has no page to be titled after. It
reaches whatever its own words happen to spell instead. An identifier goes to
`typo3_changelog_lookup` under its own name, which reaches the entries
writing it however the change was titled, and then to the class below. Where
the manual has no page for a surface either, that is a result and not an
answer. Undocumented is not unsupported.
A task that produces no change does not reach this step at all. The property
is what the task produces, and a triage, a reproduction and a review are
illustrations of it rather than the list it is read off. The sweep asks what
a package will have to stop calling, and a task that writes nothing is not
going to call anything. Followed literally in a triage of one issue it costs
one call per declared major per tag and none of them bears on the report.
The exemption ends where the workflow produces a change. A review asked to
make the change is that other workflow, and it starts this order again
holding the files it is about to write.
Skip the sweep only where the change touches no TYPO3 API — a code style
fixer, a CI file, an `.editorconfig`. A deprecation is a statement about API
the package calls, so a change that calls none has nothing for the sweep to
land on and it is empty before it is run. That condition is worth stating
because this is the most expensive step of the order: one call per declared
major per tag. Which side a change falls on is read off the files it touches
and never off the task it started as — one PHP file edited along the way puts
it back among the ordinary ones, and a skip there costs the deprecation no
finding would have walked into.
A report names the step it did not reach, under either exemption. A step
passed over in silence cannot be told from one that was dropped, which is
what step 2 already asks of itself.
**Then** read the checkout. Not before: listing the files first makes everything
after the listing look optional, and the conventions arrive as a footnote to a
verdict that has already formed.
## When the lookups run out
A behaviour question that survives the lookups above is read out of the
installed source rather than guessed at. What answers it is the class that
implements the behaviour and the one it inherits from. That reading is the step
after the lookups, and what it replaces is changing the code until it works.
What it settles is what this installation does and never what TYPO3 supports. So
a finding says the question could not be settled beyond the version installed,
and an answer built on the reading names the version it holds for.
## Two kinds of lookup, and neither stands in for the other
`typo3_backend_module_lookup`, `typo3_icon_lookup`, `typo3_label_lookup`,
`typo3_fluid_namespace_list` and `typo3_configuration_lookup` report what is
registered, what a path resolves to, what a value really is at runtime. They
establish the facts of this installation and they are never a verdict on it.
`typo3_hint_lookup` and `typo3_documentation_lookup` say whether those facts are
right. A subsystem confirmed by its own runtime lookup can still break every
rule that governs it, so it is not established until both were asked.
## A rule is read in both directions
It says what new code should do, and it says what this checkout is already doing
wrong. A file that has settled into the opposite of a rule is a finding, not a
local style to preserve: consistency with a project's own habit establishes
nothing about whether the habit is right.
## What the code is for is evidence, and the repository states it
A mechanism that costs something is not a defect for costing it. Before
reporting one, find what it is there for — the manual, the README, the
changelog, the setting it is driven by, the versions the package declares it
supports — and say so. Where a purpose is documented, what you have is a
trade-off to name with its cost and its alternative, not a defect; where you
cannot find one, the finding says that it could not be established rather than
that none exists. This is the other direction of the rule above, and skipping it
turns a review into a list of everything the author did on purpose.
## What a finding rests on is part of the finding
Three things carry one: a file that was read, at its path and its line; a
command that was run, with what it printed; a mechanism traced into an installed
package. Say which of the three it is. Leaving it unsaid gives a finding read
out of a CI file the weight of one with a verified line, and the reader has no
way to separate them again.
Where one of the project's own commands would settle it, run it.
`typo3_project_describe` marks each command it lists **check**, **change** or
**unknown**, read off the declared body: a check reports and hands the code back
as it was, so even a task told not to change files runs it, and the linter the
repository already declares is the cheapest evidence in it. A change is not run
under that instruction, and an unknown — a test suite, a shell pipeline, a
console command — is named in the answer as evidence that is available rather
than run unasked. What a check prints is not the finding: the configuration that
makes it fail is still what the finding is about, and the run is what takes that
finding from derived to established.
## What this server does not know
It does not read your working tree. Which files changed, which branch you are
on, and whether a path or an identifier still exists there are yours to
establish — then pass the concrete paths back, because that is what turns a
general convention into an answer about this code.
## Query it in English
The knowledge is written in English and matched lexically, so a query in another
language reaches the loanwords the two happen to share and nothing else.
Translate the subject before calling and the answer back afterwards, whatever
language you are speaking with the user.
Test strategy checklist#
# Test strategy checklist
Read this when choosing test layers, setting up or repairing their harnesses,
reviewing coverage, or selecting commands. Use only rows supported by the task
and checkout.
## Verify the starting point
- Identify the layers the requested behavior needs.
- Run or inspect each layer's discovery command before relying on it.
- Establish or repair missing prerequisites before adding coverage.
- Preserve a review-only boundary: report setup defects without changing them.
- Do not force behavior into an existing lower layer merely because the correct
layer has no harness yet.
## Select the layer
- Unit: isolated logic whose behavior needs neither TYPO3 bootstrapping nor
persistence.
- Functional: services, configuration, database schema, repositories,
DataHandler, fixtures, or interaction between TYPO3 components.
- Browser: rendered journeys, backend interaction, JavaScript, or accessibility
behavior that cannot be established below the UI.
- Static quality: defects and style no test observes — static analysis, coding
standards, structural rules. Established when the task asks for them, extended
rather than invented when it does not.
## Respect the functional frontend boundary
- A TYPO3 functional frontend subrequest proves server-side routing, rendering
and the generated response.
- An asset URL or AssetCollector tag in that response proves registration, not
that the browser loaded or executed the asset.
- Functional tests do not execute JavaScript, apply CSS, calculate layout,
exercise focus behavior or inspect the browser accessibility tree.
- Use a browser test for interactive claims, and describe untested interaction
as unverified rather than as frontend-tested.
- Documentation/configuration: validate examples and declarations directly; do
not disguise a file-presence assertion as a behavioral test.
Prefer the lowest layer that observes the contract and would fail for the
reported defect. Record why a higher layer is necessary.
## Complete setup
- Resolve development dependencies against the package's declared TYPO3 and PHP
constraints; do not copy a version from another project.
- Reuse installed template configuration and existing project conventions.
- Keep extension-owned unit and functional tests in the extension. Keep
browser-owned configuration in the project that serves the tested site.
- Establish a stable local command before CI, then make CI call that command.
- Keep a check command apart from the command that writes, and keep automatic
formatting inside the first-party paths the project intends it to touch.
- Fix a new static-analysis finding; a baseline records what was already there.
- Keep credentials and machine-specific URLs out of tracked configuration.
- Prove every newly established layer with a meaningful test or real browser
spec. When no unit-testable behavior exists, report an empty discovered suite
instead of manufacturing behavior or a vacuous assertion.
- Treat a missing runner or environment as infrastructure failure, not as a
failed product assertion.
## Select commands
1. Prefer the narrowest existing command that names the affected test or suite.
2. During setup, introduce one project-owned command per selected layer and run
it before wiring CI.
3. Use a direct PHPUnit or browser-runner invocation only when its executable,
configuration, and target path exist in the checkout.
4. Then run the containing command declared by `typo3_project_describe` or the
repository's documented test setup.
5. Never translate TYPO3 core commands into extension commands by analogy.
6. Separate missing infrastructure or environment prerequisites from a failing
test.
## Coverage review
- Trace every claimed behavior to an existing test or a concrete missing case.
- Include success, boundary, failure, authorization, and persistence behavior
only where the production code exposes those branches.
- Check that fixtures are minimal, deterministic, and independent of local site
data, time, execution order, and external services.
- Name exact files and commands; label anything not executed as unverified.
markdown
# Test strategy checklist
Read this when choosing test layers, setting up or repairing their harnesses,
reviewing coverage, or selecting commands. Use only rows supported by the task
and checkout.
## Verify the starting point
- Identify the layers the requested behavior needs.
- Run or inspect each layer's discovery command before relying on it.
- Establish or repair missing prerequisites before adding coverage.
- Preserve a review-only boundary: report setup defects without changing them.
- Do not force behavior into an existing lower layer merely because the correct
layer has no harness yet.
## Select the layer
- Unit: isolated logic whose behavior needs neither TYPO3 bootstrapping nor
persistence.
- Functional: services, configuration, database schema, repositories,
DataHandler, fixtures, or interaction between TYPO3 components.
- Browser: rendered journeys, backend interaction, JavaScript, or accessibility
behavior that cannot be established below the UI.
- Static quality: defects and style no test observes — static analysis, coding
standards, structural rules. Established when the task asks for them, extended
rather than invented when it does not.
## Respect the functional frontend boundary
- A TYPO3 functional frontend subrequest proves server-side routing, rendering
and the generated response.
- An asset URL or AssetCollector tag in that response proves registration, not
that the browser loaded or executed the asset.
- Functional tests do not execute JavaScript, apply CSS, calculate layout,
exercise focus behavior or inspect the browser accessibility tree.
- Use a browser test for interactive claims, and describe untested interaction
as unverified rather than as frontend-tested.
- Documentation/configuration: validate examples and declarations directly; do
not disguise a file-presence assertion as a behavioral test.
Prefer the lowest layer that observes the contract and would fail for the
reported defect. Record why a higher layer is necessary.
## Complete setup
- Resolve development dependencies against the package's declared TYPO3 and PHP
constraints; do not copy a version from another project.
- Reuse installed template configuration and existing project conventions.
- Keep extension-owned unit and functional tests in the extension. Keep
browser-owned configuration in the project that serves the tested site.
- Establish a stable local command before CI, then make CI call that command.
- Keep a check command apart from the command that writes, and keep automatic
formatting inside the first-party paths the project intends it to touch.
- Fix a new static-analysis finding; a baseline records what was already there.
- Keep credentials and machine-specific URLs out of tracked configuration.
- Prove every newly established layer with a meaningful test or real browser
spec. When no unit-testable behavior exists, report an empty discovered suite
instead of manufacturing behavior or a vacuous assertion.
- Treat a missing runner or environment as infrastructure failure, not as a
failed product assertion.
## Select commands
1. Prefer the narrowest existing command that names the affected test or suite.
2. During setup, introduce one project-owned command per selected layer and run
it before wiring CI.
3. Use a direct PHPUnit or browser-runner invocation only when its executable,
configuration, and target path exist in the checkout.
4. Then run the containing command declared by `typo3_project_describe` or the
repository's documented test setup.
5. Never translate TYPO3 core commands into extension commands by analogy.
6. Separate missing infrastructure or environment prerequisites from a failing
test.
## Coverage review
- Trace every claimed behavior to an existing test or a concrete missing case.
- Include success, boundary, failure, authorization, and persistence behavior
only where the production code exposes those branches.
- Check that fixtures are minimal, deterministic, and independent of local site
data, time, execution order, and external services.
- Name exact files and commands; label anything not executed as unverified.
PHPUnit unit and functional guidance#
# PHPUnit unit and functional guidance
Read this after choosing a PHPUnit layer. Let the checkout, the
`project-extension-tests` hint, and versioned documentation decide concrete
package versions, bootstrap APIs, configuration contents, and commands.
## Verify the harness
1. Inspect `composer.json`, the lock file, installed PHPUnit and
`typo3/testing-framework`, PHPUnit configuration, bootstrap files, Composer
scripts, and CI before changing them.
2. Run the narrowest declared PHPUnit discovery or suite command. Separate a
missing executable, invalid configuration, bootstrap failure, and unavailable
functional database from a failed assertion.
3. If configuration is absent or stale, take its current shape from the
installed testing framework or the versioned official documentation. Do not
copy another extension's dependency constraint or point at the core mono
repository.
4. Preserve one command per useful layer. Give CI the same entrypoint that
passes locally rather than a second invocation assembled independently.
5. When `typo3/testing-framework` is missing, use its official compatibility
information only to form candidates. Let Composer resolve the newest
candidate that intersects the package's TYPO3, PHP, PHPUnit,
minimum-stability, and lock constraints. Do not name or write a concrete
constraint until the solver has accepted it.
6. Select the functional database from existing project or CI infrastructure and
the behavior under test. SQLite is useful only when the production schema and
queries support it; it is not the default merely because it needs no service
container.
## Choose the folders
Keep an established repository layout. When none exists, start with this
extension-owned shape and adjust the copied configuration's relative paths:
```text
Build/
phpunit/
UnitTests.xml
UnitTestsBootstrap.php
FunctionalTests.xml
FunctionalTestsBootstrap.php
Tests/
Unit/
<subject path>/
<Subject>Test.php
Functional/
<subsystem>/
<Behavior>Test.php
Fixtures/
<input and expected data>
```
- Keep runner configuration and bootstraps out of `Tests/`; they describe how
the suite runs rather than one tested behavior.
- Keep unit and functional trees separate so each configuration discovers only
its own layer.
- Mirror the production subject below `Tests/Unit/` when that makes a test easy
to find. Organize functional tests by behavior or subsystem when several
production classes participate.
- Put fixtures beside the functional test or its small subsystem, not in one
extension-wide bucket whose ownership is unclear.
- Preserve the relative placement used by current testing-framework templates
when copying them. If a project already uses `Build/UnitTests.xml` or another
working shape, adapt it rather than moving files for cosmetic consistency.
- Calculate testsuite paths from the final configuration location. A path copied
from `Build/phpunit/` is wrong after moving the XML file to `Build/`, even
when the rest of the template is unchanged.
## Write the test
- Unit-test isolated behavior without booting TYPO3. Prefer real value objects
and collaborators; a test whose subject is only mocks is evidence about the
mock arrangement.
- Use a functional test for dependency injection, configuration, persistence,
repositories, TCA, DataHandler, routing, database schema, and server-side
frontend rendering.
- Load only the extensions the behavior needs, derived from package requirements
and the subject under test.
- Keep fixtures beside the test, minimal, deterministic, and explicit about
their expected result. Avoid records from the developer's installation.
- Resolve services from the functional test container when wiring is part of the
contract.
- Reset or isolate state that survives between tests. A test that passes alone
but fails in its containing suite is not finished.
- Add the smallest real assertion that proves the behavior. Never use a vacuous
assertion to certify the harness.
## Prove it
1. Run the new test or method alone.
2. Run its complete unit or functional suite.
3. Run both suites when shared bootstrap, dependencies, scripts, or CI changed.
4. Run the CI-equivalent command after local execution passes.
5. Report database or environment prerequisites separately from test results.
For a server-rendered frontend response, PHPUnit proves routing, configuration,
markup, and response behavior. It does not execute JavaScript, apply CSS, or
exercise a browser accessibility tree; use the Playwright guide for those
claims.
markdown
# PHPUnit unit and functional guidance
Read this after choosing a PHPUnit layer. Let the checkout, the
`project-extension-tests` hint, and versioned documentation decide concrete
package versions, bootstrap APIs, configuration contents, and commands.
## Verify the harness
1. Inspect `composer.json`, the lock file, installed PHPUnit and
`typo3/testing-framework`, PHPUnit configuration, bootstrap files, Composer
scripts, and CI before changing them.
2. Run the narrowest declared PHPUnit discovery or suite command. Separate a
missing executable, invalid configuration, bootstrap failure, and unavailable
functional database from a failed assertion.
3. If configuration is absent or stale, take its current shape from the
installed testing framework or the versioned official documentation. Do not
copy another extension's dependency constraint or point at the core mono
repository.
4. Preserve one command per useful layer. Give CI the same entrypoint that
passes locally rather than a second invocation assembled independently.
5. When `typo3/testing-framework` is missing, use its official compatibility
information only to form candidates. Let Composer resolve the newest
candidate that intersects the package's TYPO3, PHP, PHPUnit,
minimum-stability, and lock constraints. Do not name or write a concrete
constraint until the solver has accepted it.
6. Select the functional database from existing project or CI infrastructure and
the behavior under test. SQLite is useful only when the production schema and
queries support it; it is not the default merely because it needs no service
container.
## Choose the folders
Keep an established repository layout. When none exists, start with this
extension-owned shape and adjust the copied configuration's relative paths:
```text
Build/
phpunit/
UnitTests.xml
UnitTestsBootstrap.php
FunctionalTests.xml
FunctionalTestsBootstrap.php
Tests/
Unit/
<subject path>/
<Subject>Test.php
Functional/
<subsystem>/
<Behavior>Test.php
Fixtures/
<input and expected data>
```
- Keep runner configuration and bootstraps out of `Tests/`; they describe how
the suite runs rather than one tested behavior.
- Keep unit and functional trees separate so each configuration discovers only
its own layer.
- Mirror the production subject below `Tests/Unit/` when that makes a test easy
to find. Organize functional tests by behavior or subsystem when several
production classes participate.
- Put fixtures beside the functional test or its small subsystem, not in one
extension-wide bucket whose ownership is unclear.
- Preserve the relative placement used by current testing-framework templates
when copying them. If a project already uses `Build/UnitTests.xml` or another
working shape, adapt it rather than moving files for cosmetic consistency.
- Calculate testsuite paths from the final configuration location. A path copied
from `Build/phpunit/` is wrong after moving the XML file to `Build/`, even
when the rest of the template is unchanged.
## Write the test
- Unit-test isolated behavior without booting TYPO3. Prefer real value objects
and collaborators; a test whose subject is only mocks is evidence about the
mock arrangement.
- Use a functional test for dependency injection, configuration, persistence,
repositories, TCA, DataHandler, routing, database schema, and server-side
frontend rendering.
- Load only the extensions the behavior needs, derived from package requirements
and the subject under test.
- Keep fixtures beside the test, minimal, deterministic, and explicit about
their expected result. Avoid records from the developer's installation.
- Resolve services from the functional test container when wiring is part of the
contract.
- Reset or isolate state that survives between tests. A test that passes alone
but fails in its containing suite is not finished.
- Add the smallest real assertion that proves the behavior. Never use a vacuous
assertion to certify the harness.
## Prove it
1. Run the new test or method alone.
2. Run its complete unit or functional suite.
3. Run both suites when shared bootstrap, dependencies, scripts, or CI changed.
4. Run the CI-equivalent command after local execution passes.
5. Report database or environment prerequisites separately from test results.
For a server-rendered frontend response, PHPUnit proves routing, configuration,
markup, and response behavior. It does not execute JavaScript, apply CSS, or
exercise a browser accessibility tree; use the Playwright guide for those
claims.
Playwright browser guidance#
# Playwright browser guidance
Read this after choosing a browser layer. Keep the runner with the project that
serves the TYPO3 site. Let the checkout and current official Playwright
documentation decide dependency versions, supported configuration fields, and
browser installation commands.
## Verify the harness
1. Inspect the project and frontend package manifests, lock file, Playwright
configuration, test directories, scripts, ignored artifacts, CI, and the
command that serves the site.
2. Establish the real mounted URL and whether the journey needs backend or
frontend authentication. Do not invent a page identifier or certify an
unmounted content element.
3. Run test discovery and one existing spec before editing the harness.
Distinguish runner installation, browser availability, site reachability,
authentication, and assertion failures.
4. Choose where browsers execute from evidence in the project: host, development
container, or dedicated browser image. Keep the site URL and artifact paths
reachable from that execution environment.
5. If the checkout does not decide browser execution, leave the topology
unresolved until local and CI reachability, browser persistence, and the
project's container policy decide it. Do not turn a generic host or DDEV
preference into project evidence.
## Choose the folders
Keep an established repository layout. When none exists, start with one
project-owned browser surface:
```text
playwright.config.ts
tests/
browser/
setup/
authentication.setup.ts
e2e/
<journey>.spec.ts
accessibility/
<page>.spec.ts
fixtures/
<shared fixture>.ts
playwright/
.auth/ # generated and ignored
test-results/ # generated and ignored or uploaded
playwright-report/ # generated and ignored or uploaded
```
- Keep the configuration beside the project package manifest that owns the
Playwright dependency and scripts.
- Project-owned describes the suite's lifecycle, not necessarily the repository
root. Reuse an existing frontend package manifest when it belongs to this
deployed project and can own browser commands; do not create a second manifest
only to move Playwright closer to the root. If the only manifest belongs to a
reusable extension, establish an explicit project test package instead.
- Point `testDir` at the chosen browser-test root instead of relying on
accidental discovery across PHP or frontend unit tests.
- Keep setup files distinct from journey specs, then express their order through
Playwright project dependencies.
- Keep reusable browser fixtures and page objects separate from test cases only
after a second spec needs them.
- Keep generated authentication state, traces, screenshots, videos, and reports
out of source control. Store accepted visual baselines beside their owning
specs or under an explicit snapshot path and commit only those baselines.
- If the repository already uses `Tests/Browser/`, `e2e/`, or a frontend-package
directory, retain it and map the same ownership boundaries there.
## Establish or repair it
- Put configuration and specs at the project level unless the repository already
owns them elsewhere. A standalone extension cannot prove a rendered journey
without a site that mounts it.
- Read the base URL from environment or project configuration rather than
committing a developer-specific host.
- Add stable package scripts for a targeted run, the normal suite, and any
accepted snapshot-update workflow before wiring CI.
- Keep reports, traces, screenshots, videos, and temporary authentication state
in declared artifact or ignored paths. Commit reference snapshots only when
the project deliberately uses visual regression testing.
- Use setup dependencies or fixtures for shared authenticated state instead of
logging in independently in every spec.
- Enable only the browsers and projects the task or support policy requires.
More combinations are not evidence when nobody runs or maintains them.
## Write the spec
- Test a user-visible journey or browser-only contract: navigation,
authentication, form behavior, JavaScript interaction, focus, responsive
behavior, visual output, or accessibility.
- Prefer role, label, and other user-facing locators over DOM structure or CSS
implementation details.
- Wait for observable conditions rather than fixed timeouts.
- Keep test data deterministic and clean up state the spec creates.
- Add an accessibility scan to the relevant mounted page or journey when the
task makes an accessibility claim. Do not disable a rule merely to make the
first run green.
- Review visual differences before updating snapshots; never accept baselines
blindly.
## Prove it
1. Run the new spec alone against the real served site.
2. Confirm the expected report, trace, screenshot, or snapshot path works.
3. Run the containing browser project or suite.
4. Run the same script CI will call.
5. Report untested browsers, unavailable URLs, and environment prerequisites as
unverified rather than passed.
markdown
# Playwright browser guidance
Read this after choosing a browser layer. Keep the runner with the project that
serves the TYPO3 site. Let the checkout and current official Playwright
documentation decide dependency versions, supported configuration fields, and
browser installation commands.
## Verify the harness
1. Inspect the project and frontend package manifests, lock file, Playwright
configuration, test directories, scripts, ignored artifacts, CI, and the
command that serves the site.
2. Establish the real mounted URL and whether the journey needs backend or
frontend authentication. Do not invent a page identifier or certify an
unmounted content element.
3. Run test discovery and one existing spec before editing the harness.
Distinguish runner installation, browser availability, site reachability,
authentication, and assertion failures.
4. Choose where browsers execute from evidence in the project: host, development
container, or dedicated browser image. Keep the site URL and artifact paths
reachable from that execution environment.
5. If the checkout does not decide browser execution, leave the topology
unresolved until local and CI reachability, browser persistence, and the
project's container policy decide it. Do not turn a generic host or DDEV
preference into project evidence.
## Choose the folders
Keep an established repository layout. When none exists, start with one
project-owned browser surface:
```text
playwright.config.ts
tests/
browser/
setup/
authentication.setup.ts
e2e/
<journey>.spec.ts
accessibility/
<page>.spec.ts
fixtures/
<shared fixture>.ts
playwright/
.auth/ # generated and ignored
test-results/ # generated and ignored or uploaded
playwright-report/ # generated and ignored or uploaded
```
- Keep the configuration beside the project package manifest that owns the
Playwright dependency and scripts.
- Project-owned describes the suite's lifecycle, not necessarily the repository
root. Reuse an existing frontend package manifest when it belongs to this
deployed project and can own browser commands; do not create a second manifest
only to move Playwright closer to the root. If the only manifest belongs to a
reusable extension, establish an explicit project test package instead.
- Point `testDir` at the chosen browser-test root instead of relying on
accidental discovery across PHP or frontend unit tests.
- Keep setup files distinct from journey specs, then express their order through
Playwright project dependencies.
- Keep reusable browser fixtures and page objects separate from test cases only
after a second spec needs them.
- Keep generated authentication state, traces, screenshots, videos, and reports
out of source control. Store accepted visual baselines beside their owning
specs or under an explicit snapshot path and commit only those baselines.
- If the repository already uses `Tests/Browser/`, `e2e/`, or a frontend-package
directory, retain it and map the same ownership boundaries there.
## Establish or repair it
- Put configuration and specs at the project level unless the repository already
owns them elsewhere. A standalone extension cannot prove a rendered journey
without a site that mounts it.
- Read the base URL from environment or project configuration rather than
committing a developer-specific host.
- Add stable package scripts for a targeted run, the normal suite, and any
accepted snapshot-update workflow before wiring CI.
- Keep reports, traces, screenshots, videos, and temporary authentication state
in declared artifact or ignored paths. Commit reference snapshots only when
the project deliberately uses visual regression testing.
- Use setup dependencies or fixtures for shared authenticated state instead of
logging in independently in every spec.
- Enable only the browsers and projects the task or support policy requires.
More combinations are not evidence when nobody runs or maintains them.
## Write the spec
- Test a user-visible journey or browser-only contract: navigation,
authentication, form behavior, JavaScript interaction, focus, responsive
behavior, visual output, or accessibility.
- Prefer role, label, and other user-facing locators over DOM structure or CSS
implementation details.
- Wait for observable conditions rather than fixed timeouts.
- Keep test data deterministic and clean up state the spec creates.
- Add an accessibility scan to the relevant mounted page or journey when the
task makes an accessibility claim. Do not disable a rule merely to make the
first run green.
- Review visual differences before updating snapshots; never accept baselines
blindly.
## Prove it
1. Run the new spec alone against the real served site.
2. Confirm the expected report, trace, screenshot, or snapshot path works.
3. Run the containing browser project or suite.
4. Run the same script CI will call.
5. Report untested browsers, unavailable URLs, and environment prerequisites as
unverified rather than passed.
Static analysis and coding standards guidance#
# Static analysis and coding standards guidance
Read this after choosing the static-quality layer. It covers analysis, coding
standards, and the linting or normalisation steps that run beside them. Let the
checkout, the package's declared TYPO3 and PHP range, and versioned
documentation decide concrete package versions, rule sets, configuration
contents, and commands.
## Verify what is already there
1. Inspect the package manifests, the lock file, installed analysers and fixers,
their configuration and rule sets, existing baselines, Composer scripts, the
development environment, and CI before changing any of them. Half an
infrastructure is the ordinary case: a fixer without an analyser, a lint step
without either, a configuration nothing calls.
2. Run every check that already exists, unchanged, and record its output. That
run is what every later claim is measured against, and a first analyser
report on a project that never ran one is a finding list rather than a
regression.
3. Separate a missing executable, an unreadable configuration, and a real
finding. Only the third says anything about the code.
4. Read what existing configuration deliberately excludes before widening it. A
generated directory, a vendored library, or a fixture tree with intentionally
broken code is a decision, not an oversight.
5. For a review-only request this is the whole workflow: report what is missing
or unenforced and change nothing.
## What a complete surface covers
This is the expectation the checkout is measured against, named by what each
check establishes rather than by the tool behind it. Which of them apply is
decided by what the package ships: a check whose subject the extension does not
ship is absent for a reason rather than missing.
- **Syntax** — every shipped PHP file parses on every PHP version the package
declares support for. `php -l` through a lint runner such as
`overtrue/phplint` or `php-parallel-lint/php-parallel-lint`, so one command
covers the tree.
- **Static analysis** — types, unreachable code, and calls that cannot succeed.
`phpstan/phpstan` against the extension's own paths, which is what the core
runs on itself: `GeneralUtility::makeInstance()` and its neighbours carry
`@template` annotations, so the analyser gets its types from the installed
core rather than from a TYPO3-specific analyser extension. Beside it,
`phpstan/extension-installer` wires up whatever extensions are installed,
`phpstan/phpstan-phpunit` belongs beside a PHPUnit suite,
`bnf/phpstan-psr-container` types the container's `get()` calls, and
`phpstan/phpstan-deprecation-rules` reports use of deprecated API. Establish
that a TYPO3-specific analyser extension is still maintained before adding
one: several are not, and an abandoned extension on a current core produces
false findings instead of types. `vimeo/psalm` is the alternative where a
project already runs it. Which packages to require is the whole of what this
page decides about the analyser; what goes into its configuration is not:
`typo3_hint_lookup` with `id=extension-static-analysis` answers where the file
belongs, which include it carries, the constants an extension's analysis never
sees, the manifest excluded rather than fixed, the result cache directory, the
level, and what a baseline is for. It is read off the packages that configure
themselves this way, so ask it rather than recalling a configuration from
another project.
- **Coding standards** — the TYPO3 coding guidelines as the project applies
them. `friendsofphp/php-cs-fixer` driven by the `typo3/coding-standards` rule
set, which also owns the file header the guidelines require.
`editorconfig-checker` where the repository ships an `.editorconfig`, and
`squizlabs/php_codesniffer` with `phpcompatibility/php-compatibility` where
the declared PHP range has to be proven without running a matrix.
- **Manifests and dependencies** — `composer validate` on the extension's own
manifest and its agreement with what the extension declares about itself
elsewhere, `composer audit` for advisories against what it requires, and
`ergebnis/composer-normalize` where the project keeps its manifest normalised.
- **Shipped configuration and data** — the files the package actually ships: the
XLIFF linter `symfony/translation` ships, which a TYPO3 project usually
already has installed and which validates against the schema rather than only
parsing the XML; a YAML lint such as `j13k/yaml-lint` or the framework's own
`lint:yaml` for configuration; and `helmich/typo3-typoscript-lint` for
TypoScript. Two of them have to be told about this project before their
verdict is worth anything: `typo3_hint_lookup` with `id=language-files` says
what the XLIFF linter needs before it accepts a file named the way TYPO3 names
one, and a linter ships a configuration of its own that yours is merged over,
so what it reports and what it advises are its defaults rather than this
project's conventions — advice written for an older TYPO3 is still advice the
tool gives today. Fluid templates have no established linter — they are proven
by the functional tests that render them, and saying so is better than
inventing a check for them.
- **Shipped frontend assets** — where the package ships JavaScript, TypeScript
or CSS. `eslint` for the scripts, with `@typescript-eslint` where the sources
are TypeScript; `stylelint` for the stylesheets, with `stylelint-scss` and
`stylelint-order` where Sass is compiled; a formatter such as `prettier` on
the fix side and never wired into the check. Lint the sources the repository
maintains rather than the compiled bundle below `Resources/Public/` — a
finding in generated output is a finding about the build step that produced
it. Declare each of them as a script in the package's own `package.json`, so
the same one command exists locally and in CI, and take the Node version from
what the project declares rather than from the machine that happens to run it.
Run each one over the files it guards before reporting the entry as covered: a
linter that finds nothing in the only sources the package ships is a gap
dressed as coverage, which is the standard this page already applies to a
matrix whose cells run only version-independent steps.
Read the names as the default per check where the checkout covers it with
nothing, never as a replacement for what it already runs: a project with another
analyser, another lint runner or its own wrapper around one has answered that
question already, and a second tool for the same check is a second answer to
maintain. Versions come from the solver and from what the package's range
allows, never from this list.
`rector/rector` with `ssch/typo3-rector` reads like a check in its dry-run mode
and is not one: it proposes migrations, which belong to an upgrade task rather
than to the surface CI has to keep green.
Then say which axis each one has, because that is what decides where it runs.
Syntax and analysis depend on the PHP and TYPO3 combination and belong in the
matrix; a standards, manifest or format check is version-independent and one run
of it proves as much as sixteen. A matrix whose every cell runs only
version-independent steps proves that the files parse and nothing more — say so,
with what it costs and what it does not buy.
## Resolve the dependencies
1. Form candidates from the package's own declared TYPO3 and PHP range, never
from another project's manifest and never from the core's build tooling.
2. Let Composer resolve the newest candidate that intersects those constraints
together with what is already installed. Do not write a concrete constraint
until the solver has accepted it.
3. Take a rule set from a package the project already requires before adding
another one. Every added package is a further thing that has to stay current
across the whole declared range, which is what an abandoned one stops doing.
4. Keep every one of them in the development requirements.
5. Where the solver refuses one, read what it refused rather than which TYPO3
version is installed. A check tool has to meet what this project resolved,
and a package nothing here pins can sit a major ahead of what the framework
asks for, so the same tool goes into one project and not into the next. Where
it cannot meet it, the tool gets an installation of its own — a second
manifest below the build directory with its own vendor directory, and the
project-owned command calls that binary. The check exists either way; what
moves is where the tool's own dependencies live.
## Establish one command per check
1. Give each check one stable project-owned command, declared where the project
already declares its commands and named for what it checks rather than for
the binary behind it.
2. Keep checking and fixing apart. A check reports and fails; a fix writes. One
command that rewrites the working tree on the way to its verdict is not a
check, and it is not what CI can call.
3. Keep automatic formatting inside the first-party paths the project intends it
to touch. Vendored code, generated output, other packages' files, and
fixtures asserting exact bytes stay outside the fixer's paths — confirm that
with the fixer's own dry run before the first write and with the working
tree's status afterwards.
4. Point analysis at the paths the extension owns, at the level the project can
hold today. A level chosen for the eventual state produces a wall of findings
nobody works off.
5. Run each command locally until it passes, then make CI call that same
command. A CI step assembled independently proves something the developer
cannot reproduce.
## Work the findings off
- Fix the finding. A baseline records what was already there on the day it was
written and never receives an error the change in hand introduced.
- Where a baseline exists, read it as a work list with an owner and a horizon —
usually the release that drops the oldest supported version — and say which of
its entries the current change retires.
- A suppression that is correct while an older version is supported is evidence
rather than debt. Establish what it is there for, and what would remove it,
before proposing that it goes.
- Keep a formatting pass in its own commit, apart from behavioural change. A
diff that mixes both is one nobody can review.
- Where a check is introduced onto a repository that does not yet pass it, the
conformance commits come first and the commit that adds the check comes last,
so no commit fails the check it introduces. The obvious split does the
opposite: tooling first leaves the new check standing on a tree the
conformance pass has not reached yet. Verify it by running the check at the
new HEAD.
- Report a finding in code the task does not touch instead of quietly fixing it
alongside the requested work.
## Prove it
1. Run each check on the narrowest scope it supports, then on its full target.
2. Run the fix command, run the check again, and inspect the working tree for
files outside the intended scope.
3. Run the CI-equivalent commands after the local commands pass.
4. Report the exact commands, their results, the files the fixer changed, and
every check not run with the reason.
markdown
# Static analysis and coding standards guidance
Read this after choosing the static-quality layer. It covers analysis, coding
standards, and the linting or normalisation steps that run beside them. Let the
checkout, the package's declared TYPO3 and PHP range, and versioned
documentation decide concrete package versions, rule sets, configuration
contents, and commands.
## Verify what is already there
1. Inspect the package manifests, the lock file, installed analysers and fixers,
their configuration and rule sets, existing baselines, Composer scripts, the
development environment, and CI before changing any of them. Half an
infrastructure is the ordinary case: a fixer without an analyser, a lint step
without either, a configuration nothing calls.
2. Run every check that already exists, unchanged, and record its output. That
run is what every later claim is measured against, and a first analyser
report on a project that never ran one is a finding list rather than a
regression.
3. Separate a missing executable, an unreadable configuration, and a real
finding. Only the third says anything about the code.
4. Read what existing configuration deliberately excludes before widening it. A
generated directory, a vendored library, or a fixture tree with intentionally
broken code is a decision, not an oversight.
5. For a review-only request this is the whole workflow: report what is missing
or unenforced and change nothing.
## What a complete surface covers
This is the expectation the checkout is measured against, named by what each
check establishes rather than by the tool behind it. Which of them apply is
decided by what the package ships: a check whose subject the extension does not
ship is absent for a reason rather than missing.
- **Syntax** — every shipped PHP file parses on every PHP version the package
declares support for. `php -l` through a lint runner such as
`overtrue/phplint` or `php-parallel-lint/php-parallel-lint`, so one command
covers the tree.
- **Static analysis** — types, unreachable code, and calls that cannot succeed.
`phpstan/phpstan` against the extension's own paths, which is what the core
runs on itself: `GeneralUtility::makeInstance()` and its neighbours carry
`@template` annotations, so the analyser gets its types from the installed
core rather than from a TYPO3-specific analyser extension. Beside it,
`phpstan/extension-installer` wires up whatever extensions are installed,
`phpstan/phpstan-phpunit` belongs beside a PHPUnit suite,
`bnf/phpstan-psr-container` types the container's `get()` calls, and
`phpstan/phpstan-deprecation-rules` reports use of deprecated API. Establish
that a TYPO3-specific analyser extension is still maintained before adding
one: several are not, and an abandoned extension on a current core produces
false findings instead of types. `vimeo/psalm` is the alternative where a
project already runs it. Which packages to require is the whole of what this
page decides about the analyser; what goes into its configuration is not:
`typo3_hint_lookup` with `id=extension-static-analysis` answers where the file
belongs, which include it carries, the constants an extension's analysis never
sees, the manifest excluded rather than fixed, the result cache directory, the
level, and what a baseline is for. It is read off the packages that configure
themselves this way, so ask it rather than recalling a configuration from
another project.
- **Coding standards** — the TYPO3 coding guidelines as the project applies
them. `friendsofphp/php-cs-fixer` driven by the `typo3/coding-standards` rule
set, which also owns the file header the guidelines require.
`editorconfig-checker` where the repository ships an `.editorconfig`, and
`squizlabs/php_codesniffer` with `phpcompatibility/php-compatibility` where
the declared PHP range has to be proven without running a matrix.
- **Manifests and dependencies** — `composer validate` on the extension's own
manifest and its agreement with what the extension declares about itself
elsewhere, `composer audit` for advisories against what it requires, and
`ergebnis/composer-normalize` where the project keeps its manifest normalised.
- **Shipped configuration and data** — the files the package actually ships: the
XLIFF linter `symfony/translation` ships, which a TYPO3 project usually
already has installed and which validates against the schema rather than only
parsing the XML; a YAML lint such as `j13k/yaml-lint` or the framework's own
`lint:yaml` for configuration; and `helmich/typo3-typoscript-lint` for
TypoScript. Two of them have to be told about this project before their
verdict is worth anything: `typo3_hint_lookup` with `id=language-files` says
what the XLIFF linter needs before it accepts a file named the way TYPO3 names
one, and a linter ships a configuration of its own that yours is merged over,
so what it reports and what it advises are its defaults rather than this
project's conventions — advice written for an older TYPO3 is still advice the
tool gives today. Fluid templates have no established linter — they are proven
by the functional tests that render them, and saying so is better than
inventing a check for them.
- **Shipped frontend assets** — where the package ships JavaScript, TypeScript
or CSS. `eslint` for the scripts, with `@typescript-eslint` where the sources
are TypeScript; `stylelint` for the stylesheets, with `stylelint-scss` and
`stylelint-order` where Sass is compiled; a formatter such as `prettier` on
the fix side and never wired into the check. Lint the sources the repository
maintains rather than the compiled bundle below `Resources/Public/` — a
finding in generated output is a finding about the build step that produced
it. Declare each of them as a script in the package's own `package.json`, so
the same one command exists locally and in CI, and take the Node version from
what the project declares rather than from the machine that happens to run it.
Run each one over the files it guards before reporting the entry as covered: a
linter that finds nothing in the only sources the package ships is a gap
dressed as coverage, which is the standard this page already applies to a
matrix whose cells run only version-independent steps.
Read the names as the default per check where the checkout covers it with
nothing, never as a replacement for what it already runs: a project with another
analyser, another lint runner or its own wrapper around one has answered that
question already, and a second tool for the same check is a second answer to
maintain. Versions come from the solver and from what the package's range
allows, never from this list.
`rector/rector` with `ssch/typo3-rector` reads like a check in its dry-run mode
and is not one: it proposes migrations, which belong to an upgrade task rather
than to the surface CI has to keep green.
Then say which axis each one has, because that is what decides where it runs.
Syntax and analysis depend on the PHP and TYPO3 combination and belong in the
matrix; a standards, manifest or format check is version-independent and one run
of it proves as much as sixteen. A matrix whose every cell runs only
version-independent steps proves that the files parse and nothing more — say so,
with what it costs and what it does not buy.
## Resolve the dependencies
1. Form candidates from the package's own declared TYPO3 and PHP range, never
from another project's manifest and never from the core's build tooling.
2. Let Composer resolve the newest candidate that intersects those constraints
together with what is already installed. Do not write a concrete constraint
until the solver has accepted it.
3. Take a rule set from a package the project already requires before adding
another one. Every added package is a further thing that has to stay current
across the whole declared range, which is what an abandoned one stops doing.
4. Keep every one of them in the development requirements.
5. Where the solver refuses one, read what it refused rather than which TYPO3
version is installed. A check tool has to meet what this project resolved,
and a package nothing here pins can sit a major ahead of what the framework
asks for, so the same tool goes into one project and not into the next. Where
it cannot meet it, the tool gets an installation of its own — a second
manifest below the build directory with its own vendor directory, and the
project-owned command calls that binary. The check exists either way; what
moves is where the tool's own dependencies live.
## Establish one command per check
1. Give each check one stable project-owned command, declared where the project
already declares its commands and named for what it checks rather than for
the binary behind it.
2. Keep checking and fixing apart. A check reports and fails; a fix writes. One
command that rewrites the working tree on the way to its verdict is not a
check, and it is not what CI can call.
3. Keep automatic formatting inside the first-party paths the project intends it
to touch. Vendored code, generated output, other packages' files, and
fixtures asserting exact bytes stay outside the fixer's paths — confirm that
with the fixer's own dry run before the first write and with the working
tree's status afterwards.
4. Point analysis at the paths the extension owns, at the level the project can
hold today. A level chosen for the eventual state produces a wall of findings
nobody works off.
5. Run each command locally until it passes, then make CI call that same
command. A CI step assembled independently proves something the developer
cannot reproduce.
## Work the findings off
- Fix the finding. A baseline records what was already there on the day it was
written and never receives an error the change in hand introduced.
- Where a baseline exists, read it as a work list with an owner and a horizon —
usually the release that drops the oldest supported version — and say which of
its entries the current change retires.
- A suppression that is correct while an older version is supported is evidence
rather than debt. Establish what it is there for, and what would remove it,
before proposing that it goes.
- Keep a formatting pass in its own commit, apart from behavioural change. A
diff that mixes both is one nobody can review.
- Where a check is introduced onto a repository that does not yet pass it, the
conformance commits come first and the commit that adds the check comes last,
so no commit fails the check it introduces. The obvious split does the
opposite: tooling first leaves the new check standing on a tree the
conformance pass has not reached yet. Verify it by running the check at the
new HEAD.
- Report a finding in code the task does not touch instead of quietly fixing it
alongside the requested work.
## Prove it
1. Run each check on the narrowest scope it supports, then on its full target.
2. Run the fix command, run the check again, and inspect the working tree for
files outside the intended scope.
3. Run the CI-equivalent commands after the local commands pass.
4. Report the exact commands, their results, the files the fixer changed, and
every check not run with the reason.