Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:33 +08:00
commit 624998dbc3
27 changed files with 6036 additions and 0 deletions

154
templates/AGENTS.md Normal file
View File

@@ -0,0 +1,154 @@
# Testing Context for AI Assistants
This directory contains tests for the TYPO3 extension.
## Test Type
**[Unit|Functional|Acceptance]** tests
## Test Strategy
<!-- Describe what this directory tests and why -->
<!-- Example: "Unit tests for domain models - validates business logic without database" -->
<!-- Example: "Functional tests for repositories - verifies database queries and persistence" -->
<!-- Example: "Acceptance tests for checkout workflow - validates complete user journey from cart to payment" -->
**Scope:**
**Key Scenarios:**
**Not Covered:** <!-- What is intentionally not tested here -->
## Testing Framework
- **TYPO3 Testing Framework** (typo3/testing-framework)
- **PHPUnit** for assertions and test execution
- **[Additional tools for this test type]:**
- Unit: Prophecy for mocking
- Functional: CSV fixtures for database data
- Acceptance: Codeception + Selenium for browser automation
## Test Structure
### Base Class
Tests in this directory extend:
- **Unit**: `TYPO3\TestingFramework\Core\Unit\UnitTestCase`
- **Functional**: `TYPO3\TestingFramework\Core\Functional\FunctionalTestCase`
- **Acceptance**: Codeception Cest classes
### Naming Convention
- **Unit/Functional**: `*Test.php` (e.g., `ProductTest.php`, `ProductRepositoryTest.php`)
- **Acceptance**: `*Cest.php` (e.g., `LoginCest.php`, `CheckoutCest.php`)
## Key Patterns
### setUp() and tearDown()
```php
protected function setUp(): void
{
parent::setUp();
// Initialize test dependencies
}
protected function tearDown(): void
{
// Clean up resources
parent::tearDown();
}
```
### Assertions
Use specific assertions over generic ones:
- `self::assertTrue()`, `self::assertFalse()` for booleans
- `self::assertSame()` for strict equality
- `self::assertInstanceOf()` for type checks
- `self::assertCount()` for arrays/collections
### Fixtures (Functional Tests Only)
```php
$this->importCSVDataSet(__DIR__ . '/../Fixtures/MyFixture.csv');
```
**Fixture Files:** `Tests/Functional/Fixtures/`
**Strategy:**
- Keep fixtures minimal (only required data)
- One fixture per test scenario
- Document fixture contents in test or below
### Mocking (Unit Tests Only)
```php
use Prophecy\PhpUnit\ProphecyTrait;
$repository = $this->prophesize(UserRepository::class);
$repository->findByEmail('test@example.com')->willReturn($user);
```
## Running Tests
```bash
# All tests in this directory
composer ci:test:php:[unit|functional|acceptance]
# Via runTests.sh
Build/Scripts/runTests.sh -s [unit|functional|acceptance]
# Specific test file
vendor/bin/phpunit Tests/[Unit|Functional]/Path/To/TestFile.php
# Specific test method
vendor/bin/phpunit --filter testMethodName
```
## Fixtures Documentation (Functional Tests)
<!-- Document what each fixture contains -->
### `Fixtures/BasicProducts.csv`
- 3 products in category 1
- 2 products in category 2
- All products visible and published
### `Fixtures/PageTree.csv`
- Root page (uid: 1)
- Products page (uid: 2, pid: 1)
- Services page (uid: 3, pid: 1)
## Test Dependencies
<!-- List any special dependencies or requirements -->
- [ ] Database (functional tests only)
- [ ] Docker (acceptance tests only)
- [ ] Specific TYPO3 extensions: <!-- list if any -->
- [ ] External services: <!-- list if any -->
## Common Issues
<!-- Document common test failures and solutions -->
**Database connection errors:**
- Verify database driver configuration in `FunctionalTests.xml`
- Check Docker database service is running
**Fixture import errors:**
- Verify CSV format (proper escaping, matching table structure)
- Check file paths are correct relative to test class
**Flaky tests:**
- Use proper waits in acceptance tests (`waitForElement`)
- Avoid timing dependencies in unit/functional tests
- Ensure test independence (no shared state)
## Resources
- [Unit Testing Guide](~/.claude/skills/typo3-testing/references/unit-testing.md)
- [Functional Testing Guide](~/.claude/skills/typo3-testing/references/functional-testing.md)
- [Acceptance Testing Guide](~/.claude/skills/typo3-testing/references/acceptance-testing.md)
- [TYPO3 Testing Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/Testing/)

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit.xsd"
bootstrap="FunctionalTestsBootstrap.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
failOnRisky="true"
colors="true">
<testsuites>
<testsuite name="Functional tests">
<directory>../../Tests/Functional/</directory>
</testsuite>
</testsuites>
<php>
<const name="TYPO3_TESTING_FUNCTIONAL_REMOVE_ERROR_HANDLER" value="true" />
<env name="TYPO3_CONTEXT" value="Testing"/>
<env name="typo3DatabaseDriver" value="mysqli" force="true"/>
<env name="typo3DatabaseHost" value="localhost" force="true"/>
<env name="typo3DatabasePort" value="3306" force="true"/>
<env name="typo3DatabaseName" value="typo3_test" force="true"/>
<env name="typo3DatabaseUsername" value="root" force="true"/>
<env name="typo3DatabasePassword" value="" force="true"/>
</php>
<coverage>
<report>
<clover outputFile="../../var/log/coverage/clover.xml"/>
<html outputDirectory="../../var/log/coverage/html"/>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
</coverage>
</phpunit>

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* Bootstrap file for TYPO3 functional tests
*
* This file initializes the testing environment for functional tests.
* It sets up the necessary directory structure and prepares the TYPO3 instance.
*/
call_user_func(static function () {
$testbase = new \TYPO3\TestingFramework\Core\Testbase();
// Define original root path
$testbase->defineOriginalRootPath();
// Create necessary directories for test execution
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests');
$testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient');
});

26
templates/UnitTests.xml Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="../../vendor/autoload.php"
cacheResult="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
failOnRisky="true"
colors="true">
<testsuites>
<testsuite name="Unit tests">
<directory>../../Tests/Unit/</directory>
</testsuite>
</testsuites>
<coverage>
<report>
<clover outputFile="../../var/log/coverage/clover.xml"/>
<html outputDirectory="../../var/log/coverage/html"/>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
</coverage>
</phpunit>

View File

@@ -0,0 +1,39 @@
paths:
tests: Tests/Acceptance
output: var/log/acceptance
data: Tests/Acceptance/_data
support: Tests/Acceptance/_support
envs: Tests/Acceptance/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
suites:
acceptance:
actor: AcceptanceTester
path: .
modules:
enabled:
- WebDriver:
url: http://web:8000
browser: chrome
host: selenium
port: 4444
wait: 2
window_size: 1920x1080
capabilities:
chromeOptions:
args: ["--no-sandbox", "--disable-dev-shm-usage"]
- \\Helper\\Acceptance
config:
WebDriver:
browser: '%BROWSER%'
settings:
shuffle: false
lint: true
colors: true
memory_limit: 1024M

View File

@@ -0,0 +1,51 @@
services:
web:
image: php:8.4-apache
container_name: typo3-test-web
volumes:
- ../../../:/var/www/html
ports:
- "8000:80"
environment:
- TYPO3_CONTEXT=Testing
- typo3DatabaseDriver=mysqli
- typo3DatabaseHost=db
- typo3DatabaseName=typo3_test
- typo3DatabaseUsername=typo3
- typo3DatabasePassword=typo3
depends_on:
db:
condition: service_healthy
networks:
- typo3-test
db:
image: mysql:8.0
container_name: typo3-test-db
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
MYSQL_USER: typo3
MYSQL_PASSWORD: typo3
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- typo3-test
selenium:
image: selenium/standalone-chrome:latest
container_name: typo3-test-selenium
ports:
- "4444:4444"
shm_size: 2gb
networks:
- typo3-test
networks:
typo3-test:
driver: bridge

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Acceptance;
use Vendor\Extension\Tests\Acceptance\AcceptanceTester;
/**
* Example acceptance test demonstrating TYPO3 testing patterns
*
* Acceptance tests use a real browser to test complete user workflows.
* They verify frontend functionality and user interactions.
*/
final class LoginCest
{
public function _before(AcceptanceTester $I): void
{
// Runs before each test method
// Setup: Import fixtures, reset state, etc.
}
public function loginAsBackendUser(AcceptanceTester $I): void
{
// Navigate to login page
$I->amOnPage('/typo3');
// Fill login form
$I->fillField('username', 'admin');
$I->fillField('password', 'password');
// Submit form
$I->click('Login');
// Verify successful login
$I->see('Dashboard');
$I->seeInCurrentUrl('/typo3/module/dashboard');
}
public function loginFailsWithInvalidCredentials(AcceptanceTester $I): void
{
$I->amOnPage('/typo3');
$I->fillField('username', 'admin');
$I->fillField('password', 'wrong_password');
$I->click('Login');
// Verify login failed
$I->see('Login error');
$I->seeInCurrentUrl('/typo3');
}
public function searchesForProducts(AcceptanceTester $I): void
{
// Navigate to product listing
$I->amOnPage('/products');
// Wait for page to load
$I->waitForElement('.product-list', 5);
// Use search
$I->fillField('#search', 'laptop');
$I->click('Search');
// Wait for results
$I->waitForElement('.search-results', 5);
// Verify search results
$I->see('laptop', '.product-title');
$I->seeNumberOfElements('.product-item', [1, 10]);
}
public function addsProductToCart(AcceptanceTester $I): void
{
$I->amOnPage('/products/1');
// Click add to cart button
$I->click('#add-to-cart');
// Wait for AJAX response
$I->waitForElement('.cart-badge', 3);
// Verify cart updated
$I->see('1', '.cart-badge');
$I->see('Product added to cart');
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Functional\Domain\Repository;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\Extension\Domain\Model\Product;
use Vendor\Extension\Domain\Repository\ProductRepository;
/**
* Example functional test demonstrating TYPO3 testing patterns
*
* Functional tests use a real database and full TYPO3 instance.
* They test repositories, controllers, and integration scenarios.
*/
final class ProductRepositoryTest extends FunctionalTestCase
{
protected ProductRepository $subject;
/**
* Extensions to load for this test
*/
protected array $testExtensionsToLoad = [
'typo3conf/ext/my_extension',
];
protected function setUp(): void
{
parent::setUp();
// Get repository from dependency injection container
$this->subject = $this->get(ProductRepository::class);
}
/**
* @test
*/
public function findsProductsByCategory(): void
{
// Import test data from CSV fixture
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Products.csv');
// Execute repository method
$products = $this->subject->findByCategory(1);
// Assert results
self::assertCount(3, $products);
self::assertInstanceOf(Product::class, $products[0]);
}
/**
* @test
*/
public function findsVisibleProductsOnly(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/ProductsWithHidden.csv');
$products = $this->subject->findAll();
// Only visible products should be returned
self::assertCount(2, $products);
foreach ($products as $product) {
self::assertFalse($product->isHidden());
}
}
/**
* @test
*/
public function persistsNewProduct(): void
{
$this->importCSVDataSet(__DIR__ . '/../Fixtures/Pages.csv');
$product = new Product();
$product->setTitle('New Product');
$product->setPrice(19.99);
$product->setPid(1);
$this->subject->add($product);
// Persist to database
$this->persistenceManager->persistAll();
// Verify product was saved
$savedProducts = $this->subject->findAll();
self::assertCount(1, $savedProducts);
self::assertSame('New Product', $savedProducts[0]->getTitle());
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Vendor\Extension\Tests\Unit\Domain\Validator;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use Vendor\Extension\Domain\Validator\EmailValidator;
/**
* Example unit test demonstrating TYPO3 testing patterns
*
* Unit tests are fast, isolated tests without external dependencies.
* They test individual components (validators, utilities, domain logic).
*/
final class EmailValidatorTest extends UnitTestCase
{
protected EmailValidator $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = new EmailValidator();
}
/**
* @test
*/
public function validEmailPassesValidation(): void
{
$result = $this->subject->validate('user@example.com');
self::assertFalse($result->hasErrors());
}
/**
* @test
*/
public function invalidEmailFailsValidation(): void
{
$result = $this->subject->validate('invalid-email');
self::assertTrue($result->hasErrors());
}
/**
* @test
* @dataProvider invalidEmailProvider
*/
public function rejectsInvalidEmails(string $email): void
{
$result = $this->subject->validate($email);
self::assertTrue($result->hasErrors(), "Email '$email' should be invalid");
}
public static function invalidEmailProvider(): array
{
return [
'missing @' => ['userexample.com'],
'missing domain' => ['user@'],
'empty string' => [''],
'spaces' => ['user @example.com'],
];
}
}

View File

@@ -0,0 +1,215 @@
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: PHP Lint
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Run PHP linting
run: composer ci:test:php:lint
phpstan:
name: PHPStan (Latest PHP)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run PHPStan
run: composer ci:test:php:phpstan
security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run Composer audit
run: composer audit
cgl:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Check code style
run: composer ci:test:php:cgl
unit:
name: Unit Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run unit tests
run: composer ci:test:php:unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./var/log/coverage/clover.xml
flags: unittests
name: unit-tests-php-${{ matrix.php }}
functional:
name: Functional Tests (PHP ${{ matrix.php }}, ${{ matrix.database }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
database: ['mysqli', 'pdo_mysql', 'postgres', 'sqlite']
exclude:
# Reduce matrix for faster execution
- php: '8.2'
database: 'pdo_mysql'
- php: '8.3'
database: 'postgres'
- php: '8.4'
database: 'pdo_mysql'
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: typo3_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: typo3_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.database == 'postgres' && 'pdo_pgsql' || 'mysqli' }}
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ~/.composer/cache
key: ${{ runner.os }}-php${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --no-progress
- name: Run functional tests
env:
typo3DatabaseDriver: ${{ matrix.database }}
typo3DatabaseHost: 127.0.0.1
typo3DatabaseName: typo3_test
typo3DatabaseUsername: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
typo3DatabasePassword: ${{ matrix.database == 'postgres' && 'postgres' || 'root' }}
run: composer ci:test:php:functional