Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions apps/e2e/src/Controller/TurboController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,57 @@
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\UX\Turbo\TurboBundle;

#[Route('/ux-turbo', name: 'app_ux_turbo_')]
final class TurboController extends AbstractController
{
#[Route('/', name: 'index')]
public function index(): Response

#[Route('/drive', name: 'drive')]
public function drive(
#[MapQueryParameter] int $page = 1,
): Response
{
return $this->render('ux_turbo/index.html.twig', [
'controller_name' => 'TurboController',
if ($page === 2) {
return $this->render('ux_turbo/drive_page_2.html.twig', [
'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
]);
}

return $this->render('ux_turbo/drive.html.twig', [
'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
]);
}


#[Route('/frame', name: 'frame')]
public function frame(): Response
{
return $this->render('ux_turbo/frame.html.twig');
}

#[Route('/frame-content', name: 'frame_content')]
public function frameContent(): Response
{
return $this->render('ux_turbo/frame_content.html.twig');
}

#[Route('/stream', name: 'stream')]
public function streamAction(Request $request): Response
{
if ($request->isMethod('POST')) {
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
return $this->render('ux_turbo/stream_response.html.twig');
}

return $this->redirectToRoute('app_ux_turbo_stream');
}

return $this->render('ux_turbo/stream.html.twig');
}
}
3 changes: 3 additions & 0 deletions apps/e2e/src/Repository/ExampleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public function __construct()
new Example(UxPackage::ChartJs, 'Pie chart with options', 'A pie chart with custom options to control the appearance and behavior.', 'app_ux_chartjs_pie_with_options'),
new Example(UxPackage::LiveComponent, 'Examples filtering', "On this page, you can filter all examples by query terms, and observe how the UI and URLs update during and after processing.", 'app_home'),
new Example(UxPackage::LiveComponent, 'Counter', 'A basic counter that you can increment or decrement.', 'app_ux_live_component_counter'),
new Example(UxPackage::Turbo, 'Turbo Drive navigation', 'Navigate between pages without full page reload using Turbo Drive.', 'app_ux_turbo_drive'),
new Example(UxPackage::Turbo, 'Turbo Frame', 'A scoped section that navigates independently from the rest of the page.', 'app_ux_turbo_frame'),
new Example(UxPackage::Turbo, 'Turbo Stream after form submit', 'Update page content with Turbo Streams after a form submission.', 'app_ux_turbo_stream'),
new Example(UxPackage::LiveComponent, 'Registration form', 'A registration form with live validation using Symfony Forms and the Validator component.', 'app_ux_live_component_registration_form'),
new Example(UxPackage::LiveComponent, 'Paginated fruits list', 'A paginated list of fruits, where the current page is persisted in the URL as a path parameter.', 'app_ux_live_component_fruits'),
new Example(UxPackage::LiveComponent, 'With DTO', 'A live component that uses a DTO to encapsulate its state.', 'app_ux_live_component_with_dto'),
Expand Down
15 changes: 15 additions & 0 deletions apps/e2e/templates/ux_turbo/drive.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends 'example.html.twig' %}

{% block example %}
<div>
<h2>Turbo Drive Navigation - Page 1</h2>
<p>This page was loaded at: <strong id="page-load-time">{{ current_time }}</strong></p>
<p>This paragraph should stay visible during navigation (no full page reload).</p>

<div class="mt-4">
<a href="{{ path(app.request.attributes.get('_route'), { page: 2 }) }}" class="btn btn-primary" id="navigate-to-page-2">
Navigate to Page 2 (with Turbo Drive)
</a>
</div>
</div>
{% endblock %}
15 changes: 15 additions & 0 deletions apps/e2e/templates/ux_turbo/drive_page_2.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends 'example.html.twig' %}

{% block example %}
<div>
<h2>Turbo Drive Navigation - Page 2</h2>
<p>This page was loaded at: <strong id="page-load-time">{{ current_time }}</strong></p>
<p>You navigated here without a full page reload thanks to Turbo Drive!</p>

<div class="mt-4">
<a href="{{ path('app_ux_turbo_drive') }}" class="btn btn-secondary" id="navigate-back">
Go back to Page 1
</a>
</div>
</div>
{% endblock %}
21 changes: 21 additions & 0 deletions apps/e2e/templates/ux_turbo/frame.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends 'example.html.twig' %}

{% block example %}
<div>
<h2>Turbo Frame Demo</h2>
<p>The frame below will navigate independently from the rest of the page.</p>

<turbo-frame id="demo-frame">
<div class="alert alert-info">
<p id="frame-initial-content">This is the initial frame content.</p>
<a href="{{ path('app_ux_turbo_frame_content') }}" class="btn btn-primary" id="load-frame-content">
Load content in frame
</a>
</div>
</turbo-frame>

<div class="mt-4">
<p id="content-outside-frame">This content is outside the frame and will not change when navigating inside the frame.</p>
</div>
</div>
{% endblock %}
8 changes: 8 additions & 0 deletions apps/e2e/templates/ux_turbo/frame_content.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<turbo-frame id="demo-frame">
<div class="alert alert-success">
<p id="frame-updated-content">The frame content has been updated! This happened without a full page reload.</p>
<a href="{{ path('app_ux_turbo_frame') }}" class="btn btn-secondary">
Go back
</a>
</div>
</turbo-frame>
20 changes: 20 additions & 0 deletions apps/e2e/templates/ux_turbo/stream.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends 'example.html.twig' %}

{% block example %}
<div>
<h2>Turbo Stream Demo</h2>
<p>Submit the form to update content with Turbo Streams.</p>

<div id="form-container">
<form method="POST" action="{{ path('app_ux_turbo_stream') }}">
<button type="submit" class="btn btn-primary" id="submit-turbo-stream">
Submit form
</button>
</form>
</div>

<div id="stream-target" class="mt-4">
<p>Content before form submission</p>
</div>
</div>
{% endblock %}
11 changes: 11 additions & 0 deletions apps/e2e/templates/ux_turbo/stream_response.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<turbo-stream action="replace" target="stream-target">
<template>
<div id="stream-target" class="alert alert-success">
<p id="updated-by-stream">This content was updated by a Turbo Stream!</p>
</div>
</template>
</turbo-stream>

<turbo-stream action="remove" target="form-container">
<template></template>
</turbo-stream>
7 changes: 0 additions & 7 deletions src/Turbo/assets/test/browser/placeholder.test.ts

This file was deleted.

89 changes: 89 additions & 0 deletions src/Turbo/assets/test/browser/turbo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { expect, type Page, test } from '@playwright/test';

/**
* Inject a marker in the window object to detect full page reloads.
* If the page is fully reloaded, this marker will disappear.
*/
async function markPageAsLoaded(page: Page): Promise<void> {
await page.evaluate(() => {
(window as any).__turboTestMarker = 'initial-load';
});
}

/**
* Verify that the page was not fully reloaded by checking if the marker is still present.
* This proves that Turbo handled the navigation without a full page reload.
*/
async function expectNoFullPageReload(page: Page): Promise<void> {
const markerStillPresent = await page.evaluate(() => {
return (window as any).__turboTestMarker === 'initial-load';
});
expect(markerStillPresent).toBe(true);
}

test('Can navigate with Turbo Drive without full page reload', async ({ page }) => {
await page.goto('/ux-turbo/drive');
await markPageAsLoaded(page);

// Check initial page content
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');
const initialTime = await page.locator('#page-load-time').textContent();

// Navigate to page 2
await page.click('#navigate-to-page-2');

// Wait for navigation to complete
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 2');

// The time on page 2 should be different (it's a new request)
const page2Time = await page.locator('#page-load-time').textContent();
expect(page2Time).not.toBe(initialTime);

// Navigate back to page 1
await page.click('#navigate-back');
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');

await expectNoFullPageReload(page);
});

test('Can navigate inside a Turbo Frame without affecting the rest of the page', async ({ page }) => {
await page.goto('/ux-turbo/frame');
await markPageAsLoaded(page);

// Check initial state
await expect(page.locator('#frame-initial-content')).toContainText('This is the initial frame content');
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');

// Click link inside the frame
await page.click('#load-frame-content');

// Wait for frame content to update
await expect(page.locator('#frame-updated-content')).toContainText('The frame content has been updated');

// Verify content outside frame hasn't changed
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');

// Verify the frame initial content is no longer visible
await expect(page.locator('#frame-initial-content')).not.toBeVisible();

await expectNoFullPageReload(page);
});

test('Can update page content with Turbo Streams after form submission', async ({ page }) => {
await page.goto('/ux-turbo/stream');
await markPageAsLoaded(page);
// Submit the form
await page.click('#submit-turbo-stream');

// Wait for Turbo Stream to update the content
await expect(page.locator('#updated-by-stream')).toContainText('This content was updated by a Turbo Stream');

// Verify the form was removed by the Turbo Stream
await expect(page.locator('#form-container')).not.toBeVisible();

// Verify the target element still exists but with new content
await expect(page.locator('#stream-target')).toBeVisible();
await expect(page.locator('#stream-target')).toHaveClass(/alert-success/);

await expectNoFullPageReload(page);
});
Loading