Skip to content

Commit abedbd1

Browse files
committed
minor #2720 [Toolkit] Add functional tests to render all Kit components usage codes (from their documentation), with a snapshot system (Kocal)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Toolkit] Add functional tests to render all Kit components usage codes (from their documentation), with a snapshot system | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Docs? | no <!-- required for new features --> | Issues | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT <!-- Replace this notice by a description of your feature/bugfix. This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - For new features, provide some code snippets to help understand usage. - Features and deprecations must be submitted against branch main. - Update/add documentation as required (we can help!) - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> This PR add snapshots testing for each Twig code blocks from `src/Toolkit/kits/**/docs/components/*.md`, to ensure that rendering is always good. I don't think we will add visual testing, at least for now. Having snapshots tests and visual previews (on the website) feels convenient enough, but if someone has a super-simple solution, why not!) I tagged the PR as a feature, not because the snapshots testing itself, but because I introduced a new service `KitContextRunner`. I needed something similar than the "hacks" from the website, but for tests. Commits ------- 7fea71c [Toolkit] Remove checks layers for tales-from-a-dev/twig-tailwind-extra on PHP < 8.2, since it now supports PHP 8.1 1b1c545 [Toolkit] Pin symfony/phpunit-bridge to ^7.2 and phpunit to ^9.6.22 114a43f [Toolkit] Remove documentation about non-existant component AlertDialog (will be re-added later 😇) 1ce9d56 [Toolkit] Add functional tests to render all Kit components usage codes (from their documentation), with a snapshot system 4314322 [Toolkit] Introduce KitContextRunner, to run code in the context of a given Kit
2 parents 22a1313 + 7fea71c commit abedbd1

File tree

82 files changed

+2033
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+2033
-137
lines changed

src/Toolkit/composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@
4444
"zenstruck/console-test": "^1.7",
4545
"symfony/http-client": "6.4|^7.0",
4646
"symfony/stopwatch": "^6.4|^7.0",
47-
"symfony/phpunit-bridge": "^6.4|^7.0",
48-
"vincentlanglet/twig-cs-fixer": "^3.5"
47+
"symfony/phpunit-bridge": "^7.2",
48+
"vincentlanglet/twig-cs-fixer": "^3.5",
49+
"spatie/phpunit-snapshot-assertions": "^4.2.17",
50+
"phpunit/phpunit": "^9.6.22",
51+
"symfony/ux-icons": "^2.18",
52+
"tales-from-a-dev/twig-tailwind-extra": "^0.4.0"
4953
},
5054
"bin": [
5155
"bin/ux-toolkit-kit-create",

src/Toolkit/config/services.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\UX\Toolkit\Command\DebugKitCommand;
1515
use Symfony\UX\Toolkit\Command\InstallComponentCommand;
16+
use Symfony\UX\Toolkit\Kit\KitContextRunner;
1617
use Symfony\UX\Toolkit\Kit\KitFactory;
1718
use Symfony\UX\Toolkit\Kit\KitSynchronizer;
1819
use Symfony\UX\Toolkit\Registry\GitHubRegistry;
@@ -75,5 +76,12 @@
7576
->args([
7677
service('filesystem'),
7778
])
79+
80+
->set('ux_toolkit.kit.kit_context_runner', KitContextRunner::class)
81+
->public()
82+
->args([
83+
service('twig'),
84+
service('ux.twig_component.component_factory'),
85+
])
7886
;
7987
};

src/Toolkit/kits/shadcn/docs/components/AlertDialog.md

Lines changed: 0 additions & 80 deletions
This file was deleted.

src/Toolkit/phpunit.xml.dist

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
backupGlobals="false"
55
colors="true"
66
bootstrap="tests/bootstrap.php"
7-
failOnRisky="true"
7+
failOnRisky="true"
88
failOnWarning="true"
99
>
1010
<php>
1111
<ini name="display_errors" value="1"/>
1212
<ini name="error_reporting" value="-1"/>
1313
<server name="APP_ENV" value="test" force="true"/>
1414
<server name="SHELL_VERBOSITY" value="-1"/>
15-
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
16-
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
1715
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[total]=999999"/>
1816
<server name="KERNEL_CLASS" value="Symfony\UX\Toolkit\Tests\Fixtures\Kernel"/>
1917
</php>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Toolkit\Kit;
13+
14+
use Symfony\Component\Filesystem\Path;
15+
use Symfony\UX\Toolkit\File\FileType;
16+
use Symfony\UX\TwigComponent\ComponentFactory;
17+
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
18+
use Twig\Loader\ChainLoader;
19+
use Twig\Loader\FilesystemLoader;
20+
21+
/**
22+
* @author Hugo Alliaume <hugo@alliau.me>
23+
*
24+
* @internal
25+
*/
26+
final class KitContextRunner
27+
{
28+
public function __construct(
29+
private readonly \Twig\Environment $twig,
30+
private readonly ComponentFactory $componentFactory,
31+
) {
32+
}
33+
34+
/**
35+
* @template TResult of mixed
36+
*
37+
* @param callable(Kit): TResult $callback
38+
*
39+
* @return TResult
40+
*/
41+
public function runForKit(Kit $kit, callable $callback): mixed
42+
{
43+
$resetServices = $this->contextualizeServicesForKit($kit);
44+
45+
try {
46+
return $callback($kit);
47+
} finally {
48+
$resetServices();
49+
}
50+
}
51+
52+
/**
53+
* @return callable(): void Reset the services when called
54+
*/
55+
private function contextualizeServicesForKit(Kit $kit): callable
56+
{
57+
// Configure Twig
58+
$initialTwigLoader = $this->twig->getLoader();
59+
$this->twig->setLoader(new ChainLoader([
60+
new FilesystemLoader(Path::join($kit->path, 'templates/components')),
61+
$initialTwigLoader,
62+
]));
63+
64+
// Configure Twig Components
65+
$reflComponentFactory = new \ReflectionClass($this->componentFactory);
66+
67+
$reflComponentFactoryConfig = $reflComponentFactory->getProperty('config');
68+
$initialComponentFactoryConfig = $reflComponentFactoryConfig->getValue($this->componentFactory);
69+
$reflComponentFactoryConfig->setValue($this->componentFactory, []);
70+
71+
$reflComponentFactoryComponentTemplateFinder = $reflComponentFactory->getProperty('componentTemplateFinder');
72+
$initialComponentFactoryComponentTemplateFinder = $reflComponentFactoryComponentTemplateFinder->getValue($this->componentFactory);
73+
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $this->createComponentTemplateFinder($kit));
74+
75+
return function () use ($initialTwigLoader, $reflComponentFactoryConfig, $initialComponentFactoryConfig, $reflComponentFactoryComponentTemplateFinder, $initialComponentFactoryComponentTemplateFinder) {
76+
$this->twig->setLoader($initialTwigLoader);
77+
$reflComponentFactoryConfig->setValue($this->componentFactory, $initialComponentFactoryConfig);
78+
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $initialComponentFactoryComponentTemplateFinder);
79+
};
80+
}
81+
82+
private function createComponentTemplateFinder(Kit $kit): ComponentTemplateFinderInterface
83+
{
84+
static $instances = [];
85+
86+
return $instances[$kit->name] ?? new class($kit) implements ComponentTemplateFinderInterface {
87+
public function __construct(private readonly Kit $kit)
88+
{
89+
}
90+
91+
public function findAnonymousComponentTemplate(string $name): ?string
92+
{
93+
if (null === $component = $this->kit->getComponent($name)) {
94+
throw new \RuntimeException(\sprintf('Component "%s" does not exist in kit "%s".', $name, $this->kit->name));
95+
}
96+
97+
foreach ($component->files as $file) {
98+
if (FileType::Twig === $file->type) {
99+
return $file->relativePathName;
100+
}
101+
}
102+
103+
throw new \LogicException(\sprintf('No Twig files found for component "%s" in kit "%s", it should not happens.', $name, $this->kit->name));
104+
}
105+
};
106+
}
107+
}

src/Toolkit/tests/Fixtures/Kernel.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
use Symfony\Bundle\TwigBundle\TwigBundle;
1717
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1818
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
19+
use Symfony\UX\Icons\UXIconsBundle;
1920
use Symfony\UX\Toolkit\UXToolkitBundle;
2021
use Symfony\UX\TwigComponent\TwigComponentBundle;
22+
use TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle;
23+
use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
2124

2225
final class Kernel extends BaseKernel
2326
{
@@ -29,6 +32,9 @@ public function registerBundles(): iterable
2932
new FrameworkBundle(),
3033
new TwigBundle(),
3134
new TwigComponentBundle(),
35+
new TwigExtraBundle(),
36+
new UXIconsBundle(),
37+
new TalesFromADevTwigExtraTailwindBundle(),
3238
new UXToolkitBundle(),
3339
];
3440
}
@@ -69,6 +75,9 @@ protected function configureContainer(ContainerConfigurator $container): void
6975

7076
->alias('ux_toolkit.registry.registry_factory', '.ux_toolkit.registry.registry_factory')
7177
->public()
78+
79+
->alias('ux_toolkit.registry.local', '.ux_toolkit.registry.local')
80+
->public()
7281
;
7382
}
7483
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Toolkit\Tests\Functional;
13+
14+
use Spatie\Snapshots\Drivers\HtmlDriver;
15+
use Spatie\Snapshots\MatchesSnapshots;
16+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
17+
use Symfony\Component\Filesystem\Path;
18+
use Symfony\Component\Finder\Finder;
19+
use Symfony\UX\Toolkit\Asset\Component;
20+
use Symfony\UX\Toolkit\Kit\Kit;
21+
use Symfony\UX\Toolkit\Kit\KitFactory;
22+
use Symfony\UX\Toolkit\Registry\LocalRegistry;
23+
24+
class ComponentsRenderingTest extends WebTestCase
25+
{
26+
use MatchesSnapshots;
27+
28+
private const KITS_DIR = __DIR__.'/../../kits';
29+
30+
/**
31+
* @return iterable<string, string, string>
32+
*/
33+
public static function provideTestComponentRendering(): iterable
34+
{
35+
foreach (LocalRegistry::getAvailableKitsName() as $kitName) {
36+
$kitDir = Path::join(__DIR__, '../../kits', $kitName, 'docs/components');
37+
$docsFinder = (new Finder())->files()->name('*.md')->in($kitDir)->depth(0);
38+
39+
foreach ($docsFinder as $docFile) {
40+
$componentName = $docFile->getFilenameWithoutExtension();
41+
42+
$codeBlockMatchesResult = preg_match_all('/```twig.*?\n(?P<code>.+?)```/s', $docFile->getContents(), $codeBlockMatches);
43+
if (false === $codeBlockMatchesResult || 0 === $codeBlockMatchesResult) {
44+
throw new \RuntimeException(\sprintf('No Twig code blocks found in file "%s"', $docFile->getRelativePathname()));
45+
}
46+
47+
foreach ($codeBlockMatches['code'] as $i => $code) {
48+
yield \sprintf('Kit %s, component %s, code #%d', $kitName, $componentName, $i + 1) => [$kitName, $componentName, $code];
49+
}
50+
}
51+
}
52+
}
53+
54+
/**
55+
* @dataProvider provideTestComponentRendering
56+
*/
57+
public function testComponentRendering(string $kitName, string $componentName, string $code): void
58+
{
59+
$twig = self::getContainer()->get('twig');
60+
$kitContextRunner = self::getContainer()->get('ux_toolkit.kit.kit_context_runner');
61+
62+
$kit = $this->instantiateKit($kitName);
63+
$template = $twig->createTemplate($code);
64+
$renderedCode = $kitContextRunner->runForKit($kit, fn () => $template->render());
65+
66+
$this->assertCodeRenderedMatchesHtmlSnapshot($kit, $kit->getComponent($componentName), $code, $renderedCode);
67+
}
68+
69+
private function instantiateKit(string $kitName): Kit
70+
{
71+
$kitFactory = self::getContainer()->get('ux_toolkit.kit.kit_factory');
72+
73+
self::assertInstanceOf(KitFactory::class, $kitFactory);
74+
75+
return $kitFactory->createKitFromAbsolutePath(Path::join(__DIR__, '../../kits', $kitName));
76+
}
77+
78+
private function assertCodeRenderedMatchesHtmlSnapshot(Kit $kit, Component $component, string $code, string $renderedCode): void
79+
{
80+
$info = \sprintf(<<<HTML
81+
<!--
82+
- Kit: %s
83+
- Component: %s
84+
- Code:
85+
```twig
86+
%s
87+
```
88+
- Rendered code (prettified for testing purposes, run "php vendor/bin/phpunit -d --update-snapshots" to update snapshots): -->
89+
HTML,
90+
$kit->name,
91+
$component->name,
92+
trim($code)
93+
);
94+
95+
$this->assertMatchesSnapshot($renderedCode, new class($info) extends HtmlDriver {
96+
public function __construct(private string $info)
97+
{
98+
}
99+
100+
public function serialize($data): string
101+
{
102+
$serialized = parent::serialize($data);
103+
$serialized = str_replace(['<html><body>', '</body></html>'], '', $serialized);
104+
$serialized = trim($serialized);
105+
106+
return $this->info."\n".$serialized;
107+
}
108+
});
109+
}
110+
}

0 commit comments

Comments
 (0)