From 20580f70763bcd1ec9208fd2bc73ff0681a2c11a Mon Sep 17 00:00:00 2001 From: jcgueriaud1 Date: Fri, 12 Dec 2025 14:48:18 +0200 Subject: [PATCH 1/2] Upload Element --- TODO.md | 4 +- .../dramafinder/element/UploadElement.java | 160 ++++++++++++++++++ .../dramafinder/tests/it/UploadViewIT.java | 82 +++++++++ .../dramafinder/tests/testuis/UploadView.java | 62 +++++++ 4 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/vaadin/addons/dramafinder/element/UploadElement.java create mode 100644 src/test/java/org/vaadin/addons/dramafinder/tests/it/UploadViewIT.java create mode 100644 src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java diff --git a/TODO.md b/TODO.md index 6265767..7473bde 100644 --- a/TODO.md +++ b/TODO.md @@ -27,6 +27,7 @@ - TextArea - TextField - Time Picker +- Upload ## Components not implemented @@ -39,7 +40,6 @@ - Message Input - Message List - Multi-Select Combobox -- Upload - Virtual List ## Shared interfaces implemented @@ -66,4 +66,4 @@ - HasSize -## Known issues \ No newline at end of file +## Known issues diff --git a/src/main/java/org/vaadin/addons/dramafinder/element/UploadElement.java b/src/main/java/org/vaadin/addons/dramafinder/element/UploadElement.java new file mode 100644 index 0000000..fac206b --- /dev/null +++ b/src/main/java/org/vaadin/addons/dramafinder/element/UploadElement.java @@ -0,0 +1,160 @@ +package org.vaadin.addons.dramafinder.element; + +import java.nio.file.Path; + +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.options.AriaRole; +import org.vaadin.addons.dramafinder.element.shared.FocusableElement; +import org.vaadin.addons.dramafinder.element.shared.HasEnabledElement; +import org.vaadin.addons.dramafinder.element.shared.HasThemeElement; +import org.vaadin.addons.dramafinder.element.shared.HasValidationPropertiesElement; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; + +/** + * PlaywrightElement for {@code vaadin-upload}. + *

+ * Provides helpers to feed files via the native file input, inspect the file + * list entries, and assert upload completion using the file row state. + * Factory lookup relies on the upload button {@link AriaRole#BUTTON button} + * accessible name. + */ +@PlaywrightElement(UploadElement.FIELD_TAG_NAME) +public class UploadElement extends VaadinElement + implements HasEnabledElement, + HasValidationPropertiesElement, HasThemeElement, FocusableElement { + + public static final String FIELD_TAG_NAME = "vaadin-upload"; + public static final String FILE_ITEM_TAG_NAME = "vaadin-upload-file"; + + /** + * Create a new {@code UploadElement}. + * + * @param locator the locator for the {@code vaadin-upload} element + */ + public UploadElement(Locator locator) { + super(locator); + } + + /** + * {@inheritDoc} + */ + @Override + public Locator getFocusLocator() { + return getUploadButtonLocator(); + } + + /** + * {@inheritDoc} + */ + @Override + public Locator getEnabledLocator() { + return getUploadButtonLocator(); + } + + /** + * Locator for the native {@code input[type=file]} element. + * + * @return the file input locator + */ + public Locator getFileInputLocator() { + return getLocator().locator("input[type=\"file\"]").first(); + } + + /** + * Locator for the primary upload button. + * + * @return the upload button locator + */ + public Locator getUploadButtonLocator() { + return getLocator().locator("vaadin-button[slot=\"add-button\"]").first(); + } + + /** + * Locator for a specific file row. + * + * @param fileName the file name to search + * @return the matching file row locator + */ + public Locator getFileItemLocator(String fileName) { + return getLocator().locator(FILE_ITEM_TAG_NAME) + .filter(new Locator.FilterOptions().setHasText(fileName)) + .first(); + } + + /** + * Locator for the status cell of a given file row. + * + * @param fileName the file name to search + * @return the matching status locator + */ + public Locator getFileStatusLocator(String fileName) { + return getFileItemLocator(fileName).locator("[part=\"status\"]").first(); + } + + /** + * Upload one or more files by feeding the hidden input. + * + * @param files file paths to upload + */ + public void uploadFiles(Path... files) { + getFileInputLocator().setInputFiles(files); + } + + /** + * Remove a file from the list using the remove button. + * + * @param fileName the file name to remove + */ + public void removeFile(String fileName) { + getFileItemLocator(fileName).locator("[part=\"remove-button\"]").first().click(); + } + + /** + * Assert that a file is listed in the upload file list. + * + * @param fileName the expected file name + */ + public void assertHasFile(String fileName) { + assertThat(getFileItemLocator(fileName)).isVisible(); + } + + /** + * Assert that a file is not present in the upload file list. + * + * @param fileName the file name that should be absent + */ + public void assertNoFile(String fileName) { + assertThat(getFileItemLocator(fileName)).isHidden(); + } + + /** + * Assert that a file row is marked complete. + * + * @param fileName the file name to check + */ + public void assertFileComplete(String fileName) { + assertThat(getFileItemLocator(fileName)).hasAttribute("complete", ""); + } + + public void assertMaxFilesReached() { + assertThat(getLocator()).hasAttribute("max-files-reached", ""); + } + + /** + * Get the {@code UploadElement} by the accessible text of its upload button. + * + * @param page the Playwright page + * @param buttonText the accessible text of the upload button (ARIA role {@code button}) + * @return the matching {@code UploadElement} + */ + public static UploadElement getByButtonText(Page page, String buttonText) { + return new UploadElement( + page.locator(FIELD_TAG_NAME) + .filter(new Locator.FilterOptions() + .setHas(page.getByRole(AriaRole.BUTTON, + new Page.GetByRoleOptions().setName(buttonText)))) + .first()); + } +} diff --git a/src/test/java/org/vaadin/addons/dramafinder/tests/it/UploadViewIT.java b/src/test/java/org/vaadin/addons/dramafinder/tests/it/UploadViewIT.java new file mode 100644 index 0000000..e07f717 --- /dev/null +++ b/src/test/java/org/vaadin/addons/dramafinder/tests/it/UploadViewIT.java @@ -0,0 +1,82 @@ +package org.vaadin.addons.dramafinder.tests.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.vaadin.addons.dramafinder.element.UploadElement; + +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class UploadViewIT extends SpringPlaywrightIT { + + @TempDir + Path tempDir; + + @Override + public String getView() { + return "upload"; + } + + @Test + public void testSingleFileUploadCompletes() throws IOException { + Path file = Files.writeString(tempDir.resolve("single.txt"), "single"); + + UploadElement upload = UploadElement.getByButtonText(page, "Select single file"); + upload.uploadFiles(file); + + upload.assertHasFile("single.txt"); + upload.assertFileComplete("single.txt"); + assertThat(page.locator("#single-upload-status")).hasText("single.txt"); + } + + @Test + public void testMultiFileUploadAndClear() throws IOException { + Path first = Files.writeString(tempDir.resolve("first.txt"), "first"); + Path second = Files.writeString(tempDir.resolve("second.txt"), "second"); + Path third = Files.writeString(tempDir.resolve("third.txt"), "third"); + + UploadElement upload = UploadElement.getByButtonText(page, "Select multiple files"); + upload.uploadFiles(first, second); + + upload.assertHasFile("first.txt"); + upload.assertHasFile("second.txt"); + upload.assertFileComplete("first.txt"); + upload.assertFileComplete("second.txt"); + + upload.uploadFiles(third); + upload.assertHasFile("third.txt"); + upload.assertFileComplete("third.txt"); + upload.assertMaxFilesReached(); + + upload.removeFile("first.txt"); + upload.assertNoFile("first.txt"); + + upload.removeFile("second.txt"); + upload.assertNoFile("second.txt"); + + upload.removeFile("third.txt"); + upload.assertNoFile("third.txt"); + + assertThat(page.locator("#multi-upload-status")).hasText("Uploaded third.txt"); + } + + @Test + public void textFileRejected() throws IOException { + Path first = Files.writeString(tempDir.resolve("first.forbidden"), "first"); + + UploadElement upload = UploadElement.getByButtonText(page, "Select multiple files"); + upload.uploadFiles(first); + + upload.assertNoFile("first.forbidden"); + + assertThat(page.locator("#multi-upload-status")).hasText("Rejected: Incorrect File Type."); + } + + +} diff --git a/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java new file mode 100644 index 0000000..9e4a0c2 --- /dev/null +++ b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java @@ -0,0 +1,62 @@ +package org.vaadin.addons.dramafinder.tests.testuis; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.upload.Upload; +import com.vaadin.flow.component.upload.UploadI18N; +import com.vaadin.flow.component.upload.receivers.MemoryBuffer; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@PageTitle("Upload Demo") +@Route(value = "upload", layout = MainLayout.class) +public class UploadView extends Main { + + public UploadView() { + createSingleUpload(); + createMultiUpload(); + } + + private void createSingleUpload() { + MemoryBuffer buffer = new MemoryBuffer(); + Upload upload = new Upload(buffer); + upload.setMaxFiles(1); + upload.setDropLabel(new Span("Drop a single file")); + upload.setUploadButton(new Button("Select single file")); + + Span status = new Span("Waiting for file"); + status.setId("single-upload-status"); + upload.addSucceededListener(event -> status.setText(event.getFileName())); + upload.addFileRejectedListener(event -> status.setText("Rejected: " + event.getErrorMessage())); + + addExample("Single upload", new Div(upload, status)); + } + + private void createMultiUpload() { + MemoryBuffer buffer = new MemoryBuffer(); + Upload upload = new Upload(buffer); + upload.setMaxFiles(3); + upload.setMaxFileSize(1000000); + upload.setDropLabel(new Span("Drop multiple files")); + upload.setUploadButton(new Button("Select multiple files")); + upload.setAcceptedFileTypes("text/plain"); + + upload.setI18n(new UploadI18N().setError(new UploadI18N.Error().setFileIsTooBig("File is too big"))); + Span status = new Span("No uploads yet"); + status.setId("multi-upload-status"); + upload.addSucceededListener(event -> status.setText("Uploaded " + event.getFileName())); + upload.addFileRejectedListener(event -> status.setText("Rejected: " + event.getErrorMessage())); + + Button clear = new Button("Clear uploads", event -> upload.clearFileList()); + + addExample("Multi upload", new Div(upload, clear, status)); + } + + private void addExample(String title, Component component) { + add(new H2(title), component); + } +} From dca54f8e4eb0b991310e2002d528cb2797c5d415 Mon Sep 17 00:00:00 2001 From: jcgueriaud1 Date: Fri, 12 Dec 2025 14:54:31 +0200 Subject: [PATCH 2/2] Remove deprecated methods --- .../dramafinder/tests/testuis/UploadView.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java index 9e4a0c2..a82bc24 100644 --- a/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java +++ b/src/test/java/org/vaadin/addons/dramafinder/tests/testuis/UploadView.java @@ -8,9 +8,9 @@ import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.component.upload.UploadI18N; -import com.vaadin.flow.component.upload.receivers.MemoryBuffer; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.streams.UploadHandler; @PageTitle("Upload Demo") @Route(value = "upload", layout = MainLayout.class) @@ -22,23 +22,29 @@ public UploadView() { } private void createSingleUpload() { - MemoryBuffer buffer = new MemoryBuffer(); - Upload upload = new Upload(buffer); + Span status = new Span("Waiting for file"); + status.setId("single-upload-status"); + Upload upload = new Upload( + UploadHandler.inMemory((metadata, data) -> { + // No-op handler, demo asserts component behavior only. + }) + .whenComplete((result, error) -> status.setText(result.fileName()))); upload.setMaxFiles(1); upload.setDropLabel(new Span("Drop a single file")); upload.setUploadButton(new Button("Select single file")); - - Span status = new Span("Waiting for file"); - status.setId("single-upload-status"); - upload.addSucceededListener(event -> status.setText(event.getFileName())); upload.addFileRejectedListener(event -> status.setText("Rejected: " + event.getErrorMessage())); addExample("Single upload", new Div(upload, status)); } private void createMultiUpload() { - MemoryBuffer buffer = new MemoryBuffer(); - Upload upload = new Upload(buffer); + Span status = new Span("No uploads yet"); + status.setId("multi-upload-status"); + Upload upload = new Upload( + UploadHandler.inMemory((metadata, data) -> { + // No-op handler, demo asserts component behavior only. + }) + .whenComplete((result, error) -> status.setText("Uploaded " + result.fileName()))); upload.setMaxFiles(3); upload.setMaxFileSize(1000000); upload.setDropLabel(new Span("Drop multiple files")); @@ -46,9 +52,6 @@ private void createMultiUpload() { upload.setAcceptedFileTypes("text/plain"); upload.setI18n(new UploadI18N().setError(new UploadI18N.Error().setFileIsTooBig("File is too big"))); - Span status = new Span("No uploads yet"); - status.setId("multi-upload-status"); - upload.addSucceededListener(event -> status.setText("Uploaded " + event.getFileName())); upload.addFileRejectedListener(event -> status.setText("Rejected: " + event.getErrorMessage())); Button clear = new Button("Clear uploads", event -> upload.clearFileList());