Skip to content

Commit 6ad697f

Browse files
committed
Add E2E tests for Turbo
1 parent deec884 commit 6ad697f

File tree

10 files changed

+208
-11
lines changed

10 files changed

+208
-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('H:i:s'),
24+
]);
25+
}
26+
27+
return $this->render('ux_turbo/drive.html.twig', [
28+
'current_time' => (new \DateTime())->format('H:i:s'),
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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
test('Can navigate with Turbo Drive without full page reload', async ({ page }) => {
4+
await page.goto('/ux-turbo/drive');
5+
6+
// Check initial page content
7+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');
8+
const initialTime = await page.locator('#page-load-time').textContent();
9+
10+
// Navigate to page 2
11+
await page.click('#navigate-to-page-2');
12+
13+
// Wait for navigation to complete
14+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 2');
15+
16+
// The time on page 2 should be different (it's a new request)
17+
const page2Time = await page.locator('#page-load-time').textContent();
18+
expect(page2Time).not.toBe(initialTime);
19+
20+
// Navigate back to page 1
21+
await page.click('#navigate-back');
22+
await expect(page.locator('h2')).toContainText('Turbo Drive Navigation - Page 1');
23+
24+
// Verify Turbo Drive is working by checking if the page didn't do a full reload
25+
// We can verify this by checking that Turbo is present
26+
const hasTurbo = await page.evaluate(() => {
27+
return typeof (window as any).Turbo !== 'undefined';
28+
});
29+
expect(hasTurbo).toBe(true);
30+
});
31+
32+
test('Can navigate inside a Turbo Frame without affecting the rest of the page', async ({ page }) => {
33+
await page.goto('/ux-turbo/frame');
34+
35+
// Check initial state
36+
await expect(page.locator('#frame-initial-content')).toContainText('This is the initial frame content');
37+
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');
38+
39+
// Click link inside the frame
40+
await page.click('#load-frame-content');
41+
42+
// Wait for frame content to update
43+
await expect(page.locator('#frame-updated-content')).toContainText('The frame content has been updated');
44+
45+
// Verify content outside frame hasn't changed
46+
await expect(page.locator('#content-outside-frame')).toContainText('This content is outside the frame');
47+
48+
// Verify the frame initial content is no longer visible
49+
await expect(page.locator('#frame-initial-content')).not.toBeVisible();
50+
});
51+
52+
test('Can update page content with Turbo Streams after form submission', async ({ page }) => {
53+
await page.goto('/ux-turbo/stream');
54+
55+
// Check initial state
56+
await expect(page.locator('#stream-target')).toContainText('Content before form submission');
57+
await expect(page.locator('#form-container')).toBeVisible();
58+
59+
// Submit the form
60+
await page.click('#submit-turbo-stream');
61+
62+
// Wait for Turbo Stream to update the content
63+
await expect(page.locator('#updated-by-stream')).toContainText('This content was updated by a Turbo Stream');
64+
65+
// Verify the form was removed by the Turbo Stream
66+
await expect(page.locator('#form-container')).not.toBeVisible();
67+
68+
// Verify the target element still exists but with new content
69+
await expect(page.locator('#stream-target')).toBeVisible();
70+
await expect(page.locator('#stream-target')).toHaveClass(/alert-success/);
71+
});

0 commit comments

Comments
 (0)