Skip to content

Commit 76c87b8

Browse files
committed
Add E2E tests for Turbo
1 parent deec884 commit 76c87b8

File tree

10 files changed

+226
-11
lines changed

10 files changed

+226
-11
lines changed

apps/e2e/src/Controller/TurboController.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,57 @@
33
namespace App\Controller;
44

55
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
6+
use Symfony\Component\HttpFoundation\Request;
67
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
79
use Symfony\Component\Routing\Attribute\Route;
10+
use Symfony\UX\Turbo\TurboBundle;
811

912
#[Route('/ux-turbo', name: 'app_ux_turbo_')]
1013
final class TurboController extends AbstractController
1114
{
12-
#[Route('/', name: 'index')]
13-
public function index(): Response
15+
16+
#[Route('/drive', name: 'drive')]
17+
public function drive(
18+
#[MapQueryParameter] int $page = 1,
19+
): Response
1420
{
15-
return $this->render('ux_turbo/index.html.twig', [
16-
'controller_name' => 'TurboController',
21+
if ($page === 2) {
22+
return $this->render('ux_turbo/drive_page_2.html.twig', [
23+
'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
24+
]);
25+
}
26+
27+
return $this->render('ux_turbo/drive.html.twig', [
28+
'current_time' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339_EXTENDED),
1729
]);
1830
}
31+
32+
33+
#[Route('/frame', name: 'frame')]
34+
public function frame(): Response
35+
{
36+
return $this->render('ux_turbo/frame.html.twig');
37+
}
38+
39+
#[Route('/frame-content', name: 'frame_content')]
40+
public function frameContent(): Response
41+
{
42+
return $this->render('ux_turbo/frame_content.html.twig');
43+
}
44+
45+
#[Route('/stream', name: 'stream')]
46+
public function streamAction(Request $request): Response
47+
{
48+
if ($request->isMethod('POST')) {
49+
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
50+
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
51+
return $this->render('ux_turbo/stream_response.html.twig');
52+
}
53+
54+
return $this->redirectToRoute('app_ux_turbo_stream');
55+
}
56+
57+
return $this->render('ux_turbo/stream.html.twig');
58+
}
1959
}

apps/e2e/src/Repository/ExampleRepository.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public function __construct()
3232
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'),
3333
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'),
3434
new Example(UxPackage::LiveComponent, 'Counter', 'A basic counter that you can increment or decrement.', 'app_ux_live_component_counter'),
35+
new Example(UxPackage::Turbo, 'Turbo Drive navigation', 'Navigate between pages without full page reload using Turbo Drive.', 'app_ux_turbo_drive'),
36+
new Example(UxPackage::Turbo, 'Turbo Frame', 'A scoped section that navigates independently from the rest of the page.', 'app_ux_turbo_frame'),
37+
new Example(UxPackage::Turbo, 'Turbo Stream after form submit', 'Update page content with Turbo Streams after a form submission.', 'app_ux_turbo_stream'),
3538
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'),
3639
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'),
3740
new Example(UxPackage::LiveComponent, 'With DTO', 'A live component that uses a DTO to encapsulate its state.', 'app_ux_live_component_with_dto'),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% extends 'example.html.twig' %}
2+
3+
{% block example %}
4+
<div>
5+
<h2>Turbo Drive Navigation - Page 1</h2>
6+
<p>This page was loaded at: <strong id="page-load-time">{{ current_time }}</strong></p>
7+
<p>This paragraph should stay visible during navigation (no full page reload).</p>
8+
9+
<div class="mt-4">
10+
<a href="{{ path(app.request.attributes.get('_route'), { page: 2 }) }}" class="btn btn-primary" id="navigate-to-page-2">
11+
Navigate to Page 2 (with Turbo Drive)
12+
</a>
13+
</div>
14+
</div>
15+
{% endblock %}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% extends 'example.html.twig' %}
2+
3+
{% block example %}
4+
<div>
5+
<h2>Turbo Drive Navigation - Page 2</h2>
6+
<p>This page was loaded at: <strong id="page-load-time">{{ current_time }}</strong></p>
7+
<p>You navigated here without a full page reload thanks to Turbo Drive!</p>
8+
9+
<div class="mt-4">
10+
<a href="{{ path('app_ux_turbo_drive') }}" class="btn btn-secondary" id="navigate-back">
11+
Go back to Page 1
12+
</a>
13+
</div>
14+
</div>
15+
{% endblock %}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% extends 'example.html.twig' %}
2+
3+
{% block example %}
4+
<div>
5+
<h2>Turbo Frame Demo</h2>
6+
<p>The frame below will navigate independently from the rest of the page.</p>
7+
8+
<turbo-frame id="demo-frame">
9+
<div class="alert alert-info">
10+
<p id="frame-initial-content">This is the initial frame content.</p>
11+
<a href="{{ path('app_ux_turbo_frame_content') }}" class="btn btn-primary" id="load-frame-content">
12+
Load content in frame
13+
</a>
14+
</div>
15+
</turbo-frame>
16+
17+
<div class="mt-4">
18+
<p id="content-outside-frame">This content is outside the frame and will not change when navigating inside the frame.</p>
19+
</div>
20+
</div>
21+
{% endblock %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<turbo-frame id="demo-frame">
2+
<div class="alert alert-success">
3+
<p id="frame-updated-content">The frame content has been updated! This happened without a full page reload.</p>
4+
<a href="{{ path('app_ux_turbo_frame') }}" class="btn btn-secondary">
5+
Go back
6+
</a>
7+
</div>
8+
</turbo-frame>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{% extends 'example.html.twig' %}
2+
3+
{% block example %}
4+
<div>
5+
<h2>Turbo Stream Demo</h2>
6+
<p>Submit the form to update content with Turbo Streams.</p>
7+
8+
<div id="form-container">
9+
<form method="POST" action="{{ path('app_ux_turbo_stream') }}">
10+
<button type="submit" class="btn btn-primary" id="submit-turbo-stream">
11+
Submit form
12+
</button>
13+
</form>
14+
</div>
15+
16+
<div id="stream-target" class="mt-4">
17+
<p>Content before form submission</p>
18+
</div>
19+
</div>
20+
{% endblock %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<turbo-stream action="replace" target="stream-target">
2+
<template>
3+
<div id="stream-target" class="alert alert-success">
4+
<p id="updated-by-stream">This content was updated by a Turbo Stream!</p>
5+
</div>
6+
</template>
7+
</turbo-stream>
8+
9+
<turbo-stream action="remove" target="form-container">
10+
<template></template>
11+
</turbo-stream>

src/Turbo/assets/test/browser/placeholder.test.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { expect, type Page, test } from '@playwright/test';
2+
3+
/**
4+
* Inject a marker in the window object to detect full page reloads.
5+
* If the page is fully reloaded, this marker will disappear.
6+
*/
7+
async function markPageAsLoaded(page: Page): Promise<void> {
8+
await page.evaluate(() => {
9+
(window as any).__turboTestMarker = 'initial-load';
10+
});
11+
}
12+
13+
/**
14+
* Verify that the page was not fully reloaded by checking if the marker is still present.
15+
* This proves that Turbo handled the navigation without a full page reload.
16+
*/
17+
async function expectNoFullPageReload(page: Page): Promise<void> {
18+
const markerStillPresent = await page.evaluate(() => {
19+
return (window as any).__turboTestMarker === 'initial-load';
20+
});
21+
expect(markerStillPresent).toBe(true);
22+
}
23+
24+
test('Can navigate with Turbo Drive without full page reload', async ({ page }) => {
25+
await page.goto('/ux-turbo/drive');
26+
await markPageAsLoaded(page);
27+
28+
// Check initial page content
29+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');
30+
const initialTime = await page.locator('#page-load-time').textContent();
31+
32+
// Navigate to page 2
33+
await page.click('#navigate-to-page-2');
34+
35+
// Wait for navigation to complete
36+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 2');
37+
38+
// The time on page 2 should be different (it's a new request)
39+
const page2Time = await page.locator('#page-load-time').textContent();
40+
expect(page2Time).not.toBe(initialTime);
41+
42+
// Navigate back to page 1
43+
await page.click('#navigate-back');
44+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');
45+
46+
await expectNoFullPageReload(page);
47+
});
48+
49+
test('Can navigate inside a Turbo Frame without affecting the rest of the page', async ({ page }) => {
50+
await page.goto('/ux-turbo/frame');
51+
await markPageAsLoaded(page);
52+
53+
// Check initial state
54+
await expect(page.locator('#frame-initial-content')).toContainText('This is the initial frame content');
55+
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');
56+
57+
// Click link inside the frame
58+
await page.click('#load-frame-content');
59+
60+
// Wait for frame content to update
61+
await expect(page.locator('#frame-updated-content')).toContainText('The frame content has been updated');
62+
63+
// Verify content outside frame hasn't changed
64+
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');
65+
66+
// Verify the frame initial content is no longer visible
67+
await expect(page.locator('#frame-initial-content')).not.toBeVisible();
68+
69+
await expectNoFullPageReload(page);
70+
});
71+
72+
test('Can update page content with Turbo Streams after form submission', async ({ page }) => {
73+
await page.goto('/ux-turbo/stream');
74+
await markPageAsLoaded(page);
75+
// Submit the form
76+
await page.click('#submit-turbo-stream');
77+
78+
// Wait for Turbo Stream to update the content
79+
await expect(page.locator('#updated-by-stream')).toContainText('This content was updated by a Turbo Stream');
80+
81+
// Verify the form was removed by the Turbo Stream
82+
await expect(page.locator('#form-container')).not.toBeVisible();
83+
84+
// Verify the target element still exists but with new content
85+
await expect(page.locator('#stream-target')).toBeVisible();
86+
await expect(page.locator('#stream-target')).toHaveClass(/alert-success/);
87+
88+
await expectNoFullPageReload(page);
89+
});

0 commit comments

Comments
 (0)