diff --git a/grails-doc/src/en/guide/upgrading/upgrading71x.adoc b/grails-doc/src/en/guide/upgrading/upgrading71x.adoc index a8511a90435..9f674e6beae 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading71x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading71x.adoc @@ -721,4 +721,38 @@ If this causes issues with existing URL mappings, you can disable it in `applica grails: urlmapping: validateWildcards: false ----- \ No newline at end of file +---- + +===== 2.10 ContainerGebSpec Context Path Support + +`ContainerGebSpec` now automatically includes the server's servlet context path in the browser's base URL. Previously, if your application configured a context path via `server.servlet.context-path`, the `ContainerGebSpec` base URL would only include the protocol, hostname, and port — causing all page navigations to miss the context path and result in 404 errors. + +Starting in Grails 7.1, the context path is looked up from the Spring `Environment` at test setup time and appended to the base URL automatically. No changes are required in your test code — Geb page URLs should remain relative to the context root (e.g., `static url = 'greeting'`), and the framework handles prepending the context path. + +====== Example + +[source,yaml] +.application.yml +---- +server: + servlet: + context-path: /myapp +---- + +[source,groovy] +---- +// Page URL is relative — no need to include /myapp +class GreetingPage extends Page { + static url = 'greeting' + static at = { title == 'Greeting' } +} + +// Navigation automatically resolves to http://host:port/myapp/greeting +@Integration +class MySpec extends ContainerGebSpec { + void 'should reach the greeting page'() { + expect: + to(GreetingPage) + } +} +---- diff --git a/grails-geb/README.md b/grails-geb/README.md index ad01f92df52..f25326fff03 100644 --- a/grails-geb/README.md +++ b/grails-geb/README.md @@ -68,6 +68,24 @@ This requires a [compatible container runtime](https://java.testcontainers.org/s If you choose to use the `ContainerGebSpec` class, as long as you have a compatible container runtime installed, you don't need to do anything else. Just run `./gradlew integrationTest` and a container will be started and configured to start a browser that can access your application under test. +#### Context Path Support + +If your application configures a servlet context path (e.g., `server.servlet.context-path: /myapp`), `ContainerGebSpec` automatically includes it in the browser's base URL. No changes are needed in your test code — page URLs remain relative to the context root: + +```yaml +# application.yml +server: + servlet: + context-path: /myapp +``` + +```groovy +class GreetingPage extends Page { + static url = '/greeting' // relative — resolves to /myapp/greeting + static at = { title == 'Greeting' } +} +``` + #### Parallel Execution Parallel execution of `ContainerGebSpec` specifications is not currently supported. diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy index 83cc4892963..40cac514b7d 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy @@ -50,6 +50,7 @@ import org.testcontainers.images.PullPolicy import org.testcontainers.utility.DockerImageName import grails.plugin.geb.serviceloader.ServiceRegistry +import grails.util.Holders import static GrailsGebSettings.DEFAULT_AT_CHECK_WAITING import static GrailsGebSettings.DEFAULT_TIMEOUT_IMPLICITLY_WAIT @@ -115,6 +116,15 @@ class WebDriverContainerHolder { } } + private static String findServerContextPath() { + try { + def applicationContext = Holders.findApplicationContext() + return applicationContext?.environment?.getProperty('server.servlet.context-path', '/') + } catch (ignored) { + return '/' + } + } + @PackageScope boolean reinitialize(IMethodInvocation methodInvocation) { def specConf = new WebDriverContainerConfiguration( @@ -285,8 +295,19 @@ class WebDriverContainerHolder { void setupBrowserUrl(IMethodInvocation methodInvocation) { if (!browser) return int hostPort = findServerPort(methodInvocation) + String contextPath = findServerContextPath() Testcontainers.exposeHostPorts(hostPort) - browser.baseUrl = "$containerConf.protocol://$containerConf.hostName:$hostPort" + String baseUrl = "$containerConf.protocol://$containerConf.hostName:$hostPort" + if (contextPath && contextPath != '/') { + if (!contextPath.startsWith('/')) { + contextPath = "/$contextPath" + } + if (!contextPath.endsWith('/')) { + contextPath = "$contextPath/" + } + baseUrl += contextPath + } + browser.baseUrl = baseUrl } private GebTestManager createTestManager() { diff --git a/grails-test-examples/geb-context-path/build.gradle b/grails-test-examples/geb-context-path/build.gradle new file mode 100644 index 00000000000..14205713c24 --- /dev/null +++ b/grails-test-examples/geb-context-path/build.gradle @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +plugins { + id 'groovy' + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.compile' +} + +apply plugin: 'org.apache.grails.gradle.grails-web' +apply plugin: 'org.apache.grails.gradle.grails-gsp' +apply plugin: 'cloud.wondrify.asset-pipeline' + +group = 'org.demo.contextpath' +version = projectVersion + +dependencies { + + implementation platform(project(':grails-bom')) + + implementation 'org.apache.grails:grails-core' + implementation 'org.apache.grails:grails-logging' + implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-interceptors' + implementation 'org.apache.grails:grails-rest-transforms' + implementation 'org.apache.grails:grails-services' + implementation 'org.apache.grails:grails-url-mappings' + implementation 'org.apache.grails:grails-web-boot' + implementation 'org.apache.grails:grails-gsp' + if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') { + implementation 'org.apache.grails:grails-sitemesh3' + } + else { + implementation 'org.apache.grails:grails-layout' + } + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.springframework.boot:spring-boot-autoconfigure' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-tomcat' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + testAndDevelopmentOnly platform(project(':grails-bom')) + testAndDevelopmentOnly 'org.webjars.npm:bootstrap' + testAndDevelopmentOnly 'org.webjars.npm:jquery' + + runtimeOnly 'cloud.wondrify:asset-pipeline-grails' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.apache.tomcat:tomcat-jdbc' + runtimeOnly 'org.fusesource.jansi:jansi' + + testImplementation 'org.apache.grails:grails-testing-support-datamapping' + testImplementation 'org.apache.grails:grails-testing-support-web' + testImplementation 'org.spockframework:spock-core' + + integrationTestImplementation testFixtures('org.apache.grails:grails-geb') +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/test-webjar-asset-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') +} diff --git a/grails-test-examples/geb-context-path/grails-app/conf/application.yml b/grails-test-examples/geb-context-path/grails-app/conf/application.yml new file mode 100644 index 00000000000..9b1791a3f06 --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/conf/application.yml @@ -0,0 +1,94 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +server: + servlet: + context-path: /myapp +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +grails: + views: + default: + codec: html + gsp: + encoding: UTF-8 + htmlcodec: xml + codecs: + expression: html + scriptlet: html + taglib: none + staticparts: none + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + pdf: application/pdf + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + codegen: + defaultPackage: org.demo.contextpath + profile: web +dataSource: + driverClassName: org.h2.Driver + username: sa + password: '' + pooled: true + jmxExport: true +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: none + url: jdbc:h2:./prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE +hibernate: + cache: + queries: false + use_second_level_cache: false + use_query_cache: false diff --git a/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/GreetingController.groovy b/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/GreetingController.groovy new file mode 100644 index 00000000000..f66b61c61a5 --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/GreetingController.groovy @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.demo.contextpath + +class GreetingController { + + def index() { + [message: 'Hello from Grails'] + } +} diff --git a/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/UrlMappings.groovy b/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/UrlMappings.groovy new file mode 100644 index 00000000000..0bee3250f85 --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/controllers/org/demo/contextpath/UrlMappings.groovy @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.demo.contextpath + +class UrlMappings { + static mappings = { + "/$controller/$action?/$id?(.$format)?"{ + constraints { + // apply constraints here + } + } + + "/"(view:"/index") + "500"(view:'/error') + "404"(view:'/notFound') + + } +} diff --git a/grails-test-examples/geb-context-path/grails-app/i18n/messages.properties b/grails-test-examples/geb-context-path/grails-app/i18n/messages.properties new file mode 100644 index 00000000000..5b5b72e72b2 --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/i18n/messages.properties @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. diff --git a/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/Application.groovy b/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/Application.groovy new file mode 100644 index 00000000000..7419c991a2b --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/Application.groovy @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.demo.contextpath + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic + +@CompileStatic +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} diff --git a/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/BootStrap.groovy b/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/BootStrap.groovy new file mode 100644 index 00000000000..0f720b306ed --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/init/org/demo/contextpath/BootStrap.groovy @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.demo.contextpath + +class BootStrap { + + def init = { + } + + def destroy = { + } +} diff --git a/grails-test-examples/geb-context-path/grails-app/views/error.gsp b/grails-test-examples/geb-context-path/grails-app/views/error.gsp new file mode 100644 index 00000000000..8d47085a5f5 --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/views/error.gsp @@ -0,0 +1,29 @@ +<%-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you 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. + --%> + + +
+${message}
+ + diff --git a/grails-test-examples/geb-context-path/grails-app/views/index.gsp b/grails-test-examples/geb-context-path/grails-app/views/index.gsp new file mode 100644 index 00000000000..47028646bee --- /dev/null +++ b/grails-test-examples/geb-context-path/grails-app/views/index.gsp @@ -0,0 +1,27 @@ +<%-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you 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. + --%> + + + +