From fad937b2164d95b2a14331e929b65019b7474174 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Thu, 2 Jan 2025 15:01:20 +0100 Subject: [PATCH] feat: cross-platform Geb FileInput module support This resolves an issue on Windows when using the file from `container.copyFileToContainer()` with the Geb FileInput module. --- .github/workflows/gradle.yml | 5 +- .../org/demo/spock/UploadController.groovy | 22 ++++++++ .../grails-app/views/upload/index.gsp | 12 +++++ .../grails-app/views/upload/store.gsp | 9 ++++ .../groovy/org/demo/spock/UploadSpec.groovy | 49 ++++++++++++++++++ .../org/demo/spock/pages/UploadPage.groovy | 15 ++++++ .../resources/assets/upload-test.txt | 1 + .../grails/plugin/geb/ContainerGebSpec.groovy | 18 ++++++- .../ContainerGebFileInputSource.groovy | 50 +++++++++++++++++++ 9 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 spock-container-test-app/grails-app/controllers/org/demo/spock/UploadController.groovy create mode 100644 spock-container-test-app/grails-app/views/upload/index.gsp create mode 100644 spock-container-test-app/grails-app/views/upload/store.gsp create mode 100644 spock-container-test-app/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy create mode 100644 spock-container-test-app/src/integration-test/groovy/org/demo/spock/pages/UploadPage.groovy create mode 100644 spock-container-test-app/src/integration-test/resources/assets/upload-test.txt create mode 100644 src/testFixtures/groovy/grails/plugin/geb/support/ContainerGebFileInputSource.groovy diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9ae8f51..ff2c0ee 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,7 +10,10 @@ on: jobs: build: name: "Build Project" - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: {{ matrix.os }} steps: - name: "📥 Checkout repository" uses: actions/checkout@v4 diff --git a/spock-container-test-app/grails-app/controllers/org/demo/spock/UploadController.groovy b/spock-container-test-app/grails-app/controllers/org/demo/spock/UploadController.groovy new file mode 100644 index 0000000..9f9a19c --- /dev/null +++ b/spock-container-test-app/grails-app/controllers/org/demo/spock/UploadController.groovy @@ -0,0 +1,22 @@ +package org.demo.spock + +import java.nio.charset.StandardCharsets + +class UploadController { + + static allowedMethods = [index: 'GET', store: 'POST'] + + def index() {} + + def store() { + def myFile = request.getFile('myFile') + def text = 'No file uploaded' + if (myFile) { + try (InputStream inputStream = myFile.getInputStream()) { + byte[] bytes = inputStream.readAllBytes() + text = new String(bytes, StandardCharsets.UTF_8) + } + } + render(view: 'store', model: [text: text]) + } +} \ No newline at end of file diff --git a/spock-container-test-app/grails-app/views/upload/index.gsp b/spock-container-test-app/grails-app/views/upload/index.gsp new file mode 100644 index 0000000..148474e --- /dev/null +++ b/spock-container-test-app/grails-app/views/upload/index.gsp @@ -0,0 +1,12 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + + + Upload Test + + + + + + + + \ No newline at end of file diff --git a/spock-container-test-app/grails-app/views/upload/store.gsp b/spock-container-test-app/grails-app/views/upload/store.gsp new file mode 100644 index 0000000..97b9106 --- /dev/null +++ b/spock-container-test-app/grails-app/views/upload/store.gsp @@ -0,0 +1,9 @@ +<%@ page contentType="text/html;charset=UTF-8" %> + + + File Uploaded + + +${text} + + \ No newline at end of file diff --git a/spock-container-test-app/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy b/spock-container-test-app/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy new file mode 100644 index 0000000..aec372e --- /dev/null +++ b/spock-container-test-app/src/integration-test/groovy/org/demo/spock/UploadSpec.groovy @@ -0,0 +1,49 @@ +package org.demo.spock + +import grails.plugin.geb.ContainerGebSpec +import grails.testing.mixin.integration.Integration +import org.demo.spock.pages.UploadPage +import spock.lang.IgnoreIf +import spock.lang.Requires + +@Integration +class UploadSpec extends ContainerGebSpec { + + @Requires({ os.windows }) + void 'should be able to upload files on a Windows host'() { + given: + to UploadPage + + when: + fileInput.file = createFileInputSource( + 'src/integration-test/resources/assets/upload-test.txt', + '/tmp/upload-test.txt' + ) + + and: + submitBtn.click() + + then: + title == 'File Uploaded' + browser.pageSource.contains('File uploaded successfully') + } + + @IgnoreIf({ os.windows }) + void 'should be able to upload files on a non-Windows host'() { + given: + to UploadPage + + when: + fileInput.file = createFileInputSource( + 'src/integration-test/resources/assets/upload-test.txt', + '/tmp/upload-test.txt' + ) + + and: + submitBtn.click() + + then: + title == 'File Uploaded' + browser.driver.pageSource.contains('File uploaded successfully') + } +} \ No newline at end of file diff --git a/spock-container-test-app/src/integration-test/groovy/org/demo/spock/pages/UploadPage.groovy b/spock-container-test-app/src/integration-test/groovy/org/demo/spock/pages/UploadPage.groovy new file mode 100644 index 0000000..a760db3 --- /dev/null +++ b/spock-container-test-app/src/integration-test/groovy/org/demo/spock/pages/UploadPage.groovy @@ -0,0 +1,15 @@ +package org.demo.spock.pages + +import geb.Page +import geb.module.FileInput + +class UploadPage extends Page { + + static url = '/upload' + static at = { title == 'Upload Test' } + + static content = { + fileInput { $('input', name: 'myFile').module(FileInput) } + submitBtn { $('input', type: 'submit') } + } +} \ No newline at end of file diff --git a/spock-container-test-app/src/integration-test/resources/assets/upload-test.txt b/spock-container-test-app/src/integration-test/resources/assets/upload-test.txt new file mode 100644 index 0000000..0c96d3b --- /dev/null +++ b/spock-container-test-app/src/integration-test/resources/assets/upload-test.txt @@ -0,0 +1 @@ +File uploaded successfully \ No newline at end of file diff --git a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy index d805bc3..59c7569 100644 --- a/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy +++ b/src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2024 original author or authors + * Copyright 2024-2025 original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import geb.report.PageSourceReporter import geb.report.Reporter import geb.test.GebTestManager import geb.transform.DynamicallyDispatchesToBrowser +import grails.plugin.geb.support.ContainerGebFileInputSource import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.images.builder.Transferable import spock.lang.Shared import spock.lang.Specification @@ -78,4 +80,18 @@ abstract class ContainerGebSpec extends Specification implements ContainerAwareD Reporter createReporter() { new CompositeReporter(new PageSourceReporter()) } + + /** + * Copies a file from the host to the container for assignment to a Geb FileInput module. + * This method is useful when you need to upload a file to a form in a Geb test and will work cross-platform. + * + * @param hostPath relative path to the file on the host + * @param containerPath absolute path to where to put the file in the container + * @return the file object to assign to the FileInput module + * @since 4.2 + */ + File createFileInputSource(String hostPath, String containerPath) { + container.copyFileToContainer(Transferable.of(new File(hostPath).bytes), containerPath) + return new ContainerGebFileInputSource(containerPath) + } } \ No newline at end of file diff --git a/src/testFixtures/groovy/grails/plugin/geb/support/ContainerGebFileInputSource.groovy b/src/testFixtures/groovy/grails/plugin/geb/support/ContainerGebFileInputSource.groovy new file mode 100644 index 0000000..d1b9f5c --- /dev/null +++ b/src/testFixtures/groovy/grails/plugin/geb/support/ContainerGebFileInputSource.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2025 original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package grails.plugin.geb.support + +/** + * A File implementation specifically for assigning to the Geb FileInput module when using ContainerGebSpec. + * This will normalize the path for cross-platform compatibility (Windows paths with in Linux containers). + * + * @author Mattias Reichel + * @since 4.2 + */ +class ContainerGebFileInputSource extends File { + + String normalizedPath + + ContainerGebFileInputSource(String path) { + super(path) + normalizedPath = normalizePath(path) + } + + @Override + String getAbsolutePath() { + return normalizedPath + } + + /** + * Normalizes the path for cross-platform compatibility (Windows paths used in Linux containers). + * + * @param path the original file path + * @return the normalized path with forward slashes + */ + protected String normalizePath(String path) { + // Replace backslashes with forward slashes for Windows compatibility + path.replaceAll('\\\\', '/') + } +}