Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LiveComponent] Tokenize classes on all allowed whitespaces #1828

Open
wants to merge 4 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/test-live-component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Symfony UX Live Components

on:
push:
paths:
- 'src/LiveComponent/**'
pull_request:
paths:
- 'src/LiveComponent/**'

jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ['8.1', '8.3']
include:
- php-version: '8.1'
dependency-version: 'lowest'
- php-version: '8.3'
dependency-version: 'highest'

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}

- name: Install root packages
uses: ramsey/composer-install@v3
with:
working-directory: ${{ github.workspace }}
dependency-versions: ${{ matrix.dependency-version }}

- name: Build root packages
run: php .github/build-packages.php
working-directory: ${{ github.workspace }}

- name: Install LiveComponent packages
uses: ramsey/composer-install@v3
with:
working-directory: src/LiveComponent
dependency-versions: ${{ matrix.dependency-version }}

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('src/LiveComponent/assets/**', 'src/LiveComponent/tests/package.json') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install JavaScript dependencies
working-directory: src/LiveComponent/tests/app
run: yarn install

- name: Build JavaScript
working-directory: src/LiveComponent/tests/app
run: yarn build

- name: Run tests
working-directory: src/LiveComponent
run: vendor/bin/simple-phpunit
env:
SYMFONY_DEPRECATIONS_HELPER: 'max[self]=1'
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
exclude:
- component: Swup # has no tests
- component: Turbo # has its own workflow (test-turbo.yml)
- component: LiveComponent # has its own workflow (test-live-component.yml)
- component: Typed # has no tests

steps:
Expand Down
6 changes: 5 additions & 1 deletion src/LiveComponent/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/.phpunit.result.cache
/composer.lock
/phpunit.xml
/drivers
/vendor/
/var/
/.phpunit.result.cache
/tests/app/var
/tests/app/public/build/
/tests/app/yarn.lock
13 changes: 2 additions & 11 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1822,17 +1822,8 @@ class ExternalMutationTracker {
}
handleClassAttributeMutation(mutation, elementChanges) {
const element = mutation.target;
const previousValue = mutation.oldValue;
const previousValues = previousValue ? previousValue.split(' ') : [];
previousValues.forEach((value, index) => {
const trimmedValue = value.trim();
if (trimmedValue !== '') {
previousValues[index] = trimmedValue;
}
else {
previousValues.splice(index, 1);
}
});
const previousValue = mutation.oldValue || '';
const previousValues = previousValue.match(/(\S+)/gu) || [];
const newValues = [].slice.call(element.classList);
const addedValues = newValues.filter((value) => !previousValues.includes(value));
const removedValues = previousValues.filter((value) => !newValues.includes(value));
Expand Down
12 changes: 2 additions & 10 deletions src/LiveComponent/assets/src/Rendering/ExternalMutationTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,8 @@ export default class {
private handleClassAttributeMutation(mutation: MutationRecord, elementChanges: ElementChanges) {
const element = mutation.target as Element;

const previousValue = mutation.oldValue;
const previousValues = previousValue ? previousValue.split(' ') : [];
previousValues.forEach((value, index) => {
const trimmedValue = value.trim();
if (trimmedValue !== '') {
previousValues[index] = trimmedValue;
} else {
previousValues.splice(index, 1);
}
});
const previousValue = mutation.oldValue || '';
const previousValues = previousValue.match(/(\S+)/gu) || [];

const newValues: string[] = [].slice.call(element.classList);
const addedValues = newValues.filter((value) => !previousValues.includes(value));
Expand Down
7 changes: 7 additions & 0 deletions src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"autoload-dev": {
"psr-4": {
"App\\": "tests/app/src/",
"Symfony\\UX\\LiveComponent\\Tests\\": "tests/"
}
},
Expand All @@ -32,23 +33,29 @@
"twig/twig": "^3.8.0"
},
"require-dev": {
"dbrekelmans/bdi": "dev-main",
"doctrine/annotations": "^1.0",
"doctrine/collections": "^1.6.8|^2.0",
"doctrine/doctrine-bundle": "^2.4.3",
"doctrine/orm": "^2.9.4",
"doctrine/persistence": "^2.5.2|^3.0",
"phpdocumentor/reflection-docblock": "5.x-dev",
"symfony/debug-bundle": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/form": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/options-resolver": "^5.4|^6.0|^7.0",
"symfony/panther": "^1.0|^2.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/security-bundle": "^5.4|^6.0|^7.0",
"symfony/serializer": "^5.4|^6.0|^7.0",
"symfony/stimulus-bundle": "^2.9.1",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
"symfony/validator": "^5.4|^6.0|^7.0",
"symfony/web-profiler-bundle": "^5.4|^6.0|^7.0",
"symfony/webpack-encore-bundle": "^2.1.1",
"zenstruck/browser": "^1.2.0",
"zenstruck/foundry": "1.37.*"
},
Expand Down
5 changes: 5 additions & 0 deletions src/LiveComponent/phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db"/>
<env name="SHELL_VERBOSITY" value="-1"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;ignoreFile=./tests/baseline-ignore"/>
<server name="PANTHER_WEB_SERVER_DIR" value="./tests/app/public" />
</php>

<testsuites>
Expand All @@ -30,4 +31,8 @@
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>

<extensions>
<extension class="Symfony\Component\Panther\ServerExtension" />
</extensions>
</phpunit>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\LiveComponent\Tests\Integration\LiveController;

use Symfony\Component\Panther\PantherTestCase;

/**
* @author Alexander Hofbauer <alex@derhofbauer.at>
*/
class LiveControllerTest extends PantherTestCase
{
public function testWhitespaceClasses(): void
{
$client = self::createPantherClient();
$client->request('GET', '/whitespace-classes');
$client->clickLink('Remove class and render');

// wait required to run live_controller JS
$client->waitFor('#changed', 10);

self::assertSelectorAttributeContains('p#content', 'class', 'second new');
}
}
3 changes: 3 additions & 0 deletions src/LiveComponent/tests/app/assets/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

import './bootstrap';
7 changes: 7 additions & 0 deletions src/LiveComponent/tests/app/assets/bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { startStimulusApp } from '@symfony/stimulus-bridge';

export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/
));
14 changes: 14 additions & 0 deletions src/LiveComponent/tests/app/assets/controllers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"controllers": {
"@symfony/ux-live-component": {
"live": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"../../../assets/dist/live.min.css": true
}
}
}
},
"entrypoints": []
}
Empty file.
27 changes: 27 additions & 0 deletions src/LiveComponent/tests/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.2",
"@symfony/stimulus-bundle": "file:../../vendor/symfony/stimulus-bundle/assets",
"@symfony/ux-live-component": "file:../../assets",
"@symfony/webpack-encore": "^4.2.0",
"core-js": "^3.0.0",
"regenerator-runtime": "^0.13.2",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-notifier": "^1.6.0"
},
"resolutions": {
"coa": "2.0.2"
},
"license": "MIT",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
}
}
28 changes: 28 additions & 0 deletions src/LiveComponent/tests/app/public/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\HttpFoundation\Request;

require __DIR__.'/../../../vendor/autoload.php';

$app = new Kernel($_SERVER['APP_ENV'] ?? 'dev', $_SERVER['APP_DEBUG'] ?? true);

if (\PHP_SAPI === 'cli') {
$application = new Application($app);
exit($application->run());
}

$request = Request::createFromGlobals();
$response = $app->handle($request);
$response->send();
$app->terminate($request, $response);
94 changes: 94 additions & 0 deletions src/LiveComponent/tests/app/src/Kernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App;

use Symfony\Bundle\DebugBundle\DebugBundle;
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\UX\LiveComponent\LiveComponentBundle;
use Symfony\UX\StimulusBundle\StimulusBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle;
use Symfony\WebpackEncoreBundle\WebpackEncoreBundle;

/**
* @author Alexander Hofbauer <alex@derhofbauer.at>
*/
class Kernel extends BaseKernel
{
use MicroKernelTrait;

public function getProjectDir(): string
{
return __DIR__.'/../';
}

public function registerBundles(): iterable
{
yield new FrameworkBundle();
yield new TwigBundle();
yield new WebpackEncoreBundle();
yield new WebProfilerBundle();
yield new DebugBundle();
yield new StimulusBundle();
yield new TwigComponentBundle();
yield new LiveComponentBundle();
}

protected function configureContainer(ContainerConfigurator $container): void
{
$container->extension('framework', [
'secret' => 'ChangeMe',
'test' => 'test' === ($_SERVER['APP_ENV'] ?? 'dev'),
'router' => [
'utf8' => true,
],
'profiler' => [
'only_exceptions' => false,
],
]);

$container->extension('web_profiler', [
'toolbar' => true,
'intercept_redirects' => false,
]);

$container->extension('twig_component', [
'anonymous_template_directory' => 'components/',
'defaults' => [
'App\\Twig\\Component\\' => 'components/',
],
]);

$container->extension('webpack_encore', ['output_path' => 'build']);

$container->services()
->defaults()
->autowire()
->autoconfigure()
->load(__NAMESPACE__.'\\Twig\\Component\\', __DIR__.'/Twig/Component');
}

protected function configureRoutes(RoutingConfigurator $routes): void
{
$routes->import('@LiveComponentBundle/config/routes.php')->prefix('/_components');
$routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt');
$routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler');

$routes->add('whitespace_classes', '/whitespace-classes')->controller(TemplateController::class)->defaults(['template' => 'whitespace_classes.html.twig']);
}
}