Skip to content

Latest commit

 

History

History
136 lines (107 loc) · 4.07 KB

component-mock.md

File metadata and controls

136 lines (107 loc) · 4.07 KB

Mocking Twig components

One of the powerful features of Twig components is to use dependency injection to inject services (like Doctrine repositories) and consume them in property getters and other methods. Let's take the FeaturedProducts component from the official documentation:

// src/Twig/Components/FeaturedProducts.php
namespace App\Twig\Components;

use App\Repository\ProductRepository;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
class FeaturedProducts
{
    private ProductRepository $productRepository;

    public function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function getProducts(): array
    {
        // an example method that returns an array of Products
        return $this->productRepository->findFeatured();
    }
}
{# templates/components/FeaturedProducts.html.twig #}
<div>
    <h3>Featured Products</h3>

    {% for product in this.products %}
        ...
    {% endfor %}
</div>

That's pretty cool, but in your Storybook you probably don't want to use the real getProducts implementation, which relies on ProductRepository. To bypass the original property resolution, you can create a Component Mock:

// src/Storybook/Mock/FeaturedProductsMock.php

namespace App\Storybook\Mock;

use App\Twig\Components\FeaturedProducts;
use Storybook\Attributes\AsComponentMock;
use Storybook\Attributes\PropertyMock;

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{
    // Mock 'products' property for all stories:
    
    #[PropertyMock] // property argument is optional and defaults to the annotated method name
    public function products()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
        ];   
    }
    
    // Or use different implementations for specific stories:
    
    #[PropertyMock(property: 'products', stories: ['featured-products--story1', 'featured-products--story2'])]
    public function getFewProducts()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
        ];   
    }
    
    #[PropertyMock(property: 'products', stories: 'featured-products--story3')]
    public function getALotOfProducts()
    {
        return [
            ['id' => 0, 'name' => 'Product 1', 'color' => 'Red'],
            ['id' => 1, 'name' => 'Product 2', 'color' => 'Green'],
            // ...
            ['id' => 99, 'name' => 'Product 99', 'color' => 'Blue'],
        ];   
    }    
}

As Component Mocks are regular services, you can inject whatever you need, for example to delegate your fixtures management to an external service:

// src/Storybook/Mock/FeaturedProductsMock.php

// ...

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{
    public function __construct(private readonly ProductFixturesProvider $fixturesProvider) 
    {
    }
    
    #[PropertyMock]
    public function products()
    {
        return $this->fixturesProvider->getSomeProducts();
    }    
}

If you need to access the original arguments passed to the method, or the original component instance, you can use the MockInvocationContext:

// src/Storybook/Mock/FeaturedProductsMock.php

// ...

use Storybook\Mock\MockInvocationContext;

#[AsComponentMock(component: FeaturedProducts::class)]
class FeaturedProductsMock
{    
    #[PropertyMock]
    public function products(MockInvocationContext $context)
    {
        $context->component->prop; // Access to the component prop
        $context->originalArgs[0]; // Access to the first argument passed to the method
    }    
}

Note:
Mocks will also bypass resolution of computed properties, but be aware that the result will not be cached.