diff --git a/apps/e2e/public/images/example.jpg b/apps/e2e/public/images/example.jpg new file mode 100644 index 00000000000..49649fb6ca3 Binary files /dev/null and b/apps/e2e/public/images/example.jpg differ diff --git a/apps/e2e/src/Controller/CropperjsController.php b/apps/e2e/src/Controller/CropperjsController.php index 0d5ea357f2a..7ea31d9dbac 100644 --- a/apps/e2e/src/Controller/CropperjsController.php +++ b/apps/e2e/src/Controller/CropperjsController.php @@ -3,17 +3,77 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\UX\Cropperjs\Factory\CropperInterface; +use Symfony\UX\Cropperjs\Form\CropperType; #[Route('/ux-cropperjs', name: 'app_ux_cropperjs_')] final class CropperjsController extends AbstractController { - #[Route('/', name: 'index')] - public function index(): Response + public function __construct( + #[Autowire('%kernel.project_dir%/public')] + private string $publicDir + ) {} + + #[Route('/crop', name: 'crop')] + public function crop(CropperInterface $cropper, Request $request): Response { - return $this->render('ux_cropperjs/index.html.twig', [ - 'controller_name' => 'CropperjsController', + $crop = $cropper->createCrop($this->publicDir . '/images/example.jpg'); + $crop->setCroppedMaxSize(800, 600); + + $form = $this->createFormBuilder(['crop' => $crop]) + ->add('crop', CropperType::class, [ + 'public_url' => '/images/example.jpg', + 'cropper_options' => [ + 'viewMode' => 1, + ], + ]) + ->getForm(); + + $form->handleRequest($request); + + $croppedImageData = null; + if ($form->isSubmitted() && $form->isValid()) { + // Get the cropped image as base64 + $croppedImageData = base64_encode($crop->getCroppedImage()); + } + + return $this->render('ux_cropperjs/crop.html.twig', [ + 'form' => $form, + 'croppedImageData' => $croppedImageData, + ]); + } + + #[Route('/crop-with-aspect-ratio', name: 'crop_with_aspect_ratio')] + public function cropWithAspectRatio(CropperInterface $cropper, Request $request): Response + { + $crop = $cropper->createCrop($this->publicDir . '/images/example.jpg'); + $crop->setCroppedMaxSize(1920, 1080); + + $form = $this->createFormBuilder(['crop' => $crop]) + ->add('crop', CropperType::class, [ + 'public_url' => '/images/example.jpg', + 'cropper_options' => [ + 'aspectRatio' => 16 / 9, + 'viewMode' => 1, + ], + ]) + ->getForm(); + + $form->handleRequest($request); + + $croppedImageData = null; + if ($form->isSubmitted() && $form->isValid()) { + // Get the cropped image as base64 + $croppedImageData = base64_encode($crop->getCroppedImage()); + } + + return $this->render('ux_cropperjs/crop.html.twig', [ + 'form' => $form, + 'croppedImageData' => $croppedImageData, ]); } } diff --git a/apps/e2e/src/Repository/ExampleRepository.php b/apps/e2e/src/Repository/ExampleRepository.php index 4977d979cde..61379893b09 100644 --- a/apps/e2e/src/Repository/ExampleRepository.php +++ b/apps/e2e/src/Repository/ExampleRepository.php @@ -30,6 +30,8 @@ public function __construct() new Example(UxPackage::ChartJs, 'Line chart with options', 'A line chart with custom options (showLines: false) that displays data points without connecting lines.', 'app_ux_chartjs_with_options'), new Example(UxPackage::ChartJs, 'Pie chart', 'A pie chart displaying data distribution across different categories.', 'app_ux_chartjs_pie'), 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::Cropperjs, 'Image cropper', 'Crop an image with Cropper.js using default options.', 'app_ux_cropperjs_crop'), + new Example(UxPackage::Cropperjs, 'Image cropper with aspect ratio', 'Crop an image with a fixed 16:9 aspect ratio constraint.', 'app_ux_cropperjs_crop_with_aspect_ratio'), 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'), diff --git a/apps/e2e/templates/ux_cropperjs/crop.html.twig b/apps/e2e/templates/ux_cropperjs/crop.html.twig new file mode 100644 index 00000000000..057c6a176f3 --- /dev/null +++ b/apps/e2e/templates/ux_cropperjs/crop.html.twig @@ -0,0 +1,26 @@ +{% extends 'example.html.twig' %} + +{% block example %} +
+
+ {% if croppedImageData %} +
+
Image cropped successfully!
+

Here is your cropped image:

+ Cropped image +
+ {% endif %} + + {{ form_start(form, { attr: { 'data-turbo': 'false' } }) }} + {{ form_row(form.crop) }} +
+ +
+ {{ form_end(form) }} +
+
+{% endblock %} diff --git a/apps/e2e/templates/ux_cropperjs/index.html.twig b/apps/e2e/templates/ux_cropperjs/index.html.twig deleted file mode 100644 index 78c01e96007..00000000000 --- a/apps/e2e/templates/ux_cropperjs/index.html.twig +++ /dev/null @@ -1,3 +0,0 @@ -{% extends 'example.html.twig' %} - -{% block example %}{% endblock %} diff --git a/src/Cropperjs/assets/test/browser/cropperjs.test.ts b/src/Cropperjs/assets/test/browser/cropperjs.test.ts new file mode 100644 index 00000000000..2806cf64ce2 --- /dev/null +++ b/src/Cropperjs/assets/test/browser/cropperjs.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from '@playwright/test'; + +test('Can display and interact with Cropper.js', async ({ page }) => { + await page.goto('/ux-cropperjs/crop'); + + // Wait for the cropper to be initialized + const cropperContainer = page.locator('.cropperjs'); + await expect(cropperContainer).toBeVisible(); + + // Check that the canvas is created by Cropper.js + const cropperCanvas = page.locator('.cropper-canvas'); + await expect(cropperCanvas).toBeVisible(); + + // Check that the crop box is visible + const cropBox = page.locator('.cropper-crop-box'); + await expect(cropBox).toBeVisible(); + + const actionE = page.locator('[data-cropper-action="e"].cropper-line'); + const actionEBox = await cropBox.boundingBox(); + if (actionEBox) { + // Drag the east handle to resize the crop box + await actionE.hover({ force: true }); + await page.mouse.down(); + await page.mouse.move(actionEBox.x + actionEBox.width - 200, actionEBox.y + actionEBox.height / 2); + await page.mouse.up(); + } + + // Submit the form to crop the image + await page.click('#crop-submit'); + + // Wait for the cropped image to be displayed + await expect(page.locator('#crop-success')).toBeVisible(); + await expect(page.locator('#cropped-result')).toBeVisible(); + + // Verify the cropped image is a valid base64 image + const croppedImage = page.locator('#cropped-result'); + expect(await croppedImage.getAttribute('src')).toContain('data:image/jpeg;base64,'); + const croppedImageWidth = await croppedImage.evaluate((img: HTMLImageElement) => img.naturalWidth); + const croppedImageHeight = await croppedImage.evaluate((img: HTMLImageElement) => img.naturalHeight); + expect(croppedImageWidth).toBeGreaterThanOrEqual(540); // Chrome + expect(croppedImageWidth).toBeLessThanOrEqual(541); // Firefox + expect(croppedImageHeight).toEqual(461); +}); + +test('Can display Cropper.js with aspect ratio constraint', async ({ page }) => { + await page.goto('/ux-cropperjs/crop-with-aspect-ratio'); + + // Wait for the cropper to be initialized + const cropperContainer = page.locator('.cropperjs'); + await expect(cropperContainer).toBeVisible(); + + // Check that the canvas and crop box are visible + await expect(page.locator('.cropper-canvas')).toBeVisible(); + await expect(page.locator('.cropper-crop-box')).toBeVisible(); + + // Submit the form to crop the image + await page.click('#crop-submit'); + + // Wait for the cropped image to be displayed + await expect(page.locator('#crop-success')).toBeVisible(); + await expect(page.locator('#cropped-result')).toBeVisible(); + + // Verify the cropped image is a valid base64 image, and has the correct aspect ratio (16:9) + const croppedImage = page.locator('#cropped-result'); + expect(await croppedImage.getAttribute('src')).toContain('data:image/jpeg;base64,'); + expect(await croppedImage.evaluate((img: HTMLImageElement) => img.naturalWidth / img.naturalHeight)).toBeCloseTo( + 16 / 9, + 1 + ); +}); diff --git a/src/Cropperjs/assets/test/browser/placeholder.test.ts b/src/Cropperjs/assets/test/browser/placeholder.test.ts deleted file mode 100644 index f5f1d10f22e..00000000000 --- a/src/Cropperjs/assets/test/browser/placeholder.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('Can see homepage', async ({ page }) => { - await page.goto('/'); - - await expect(page.getByText("Symfony UX's E2E App")).toBeVisible(); -});