Skip to content

Commit 7eae6e4

Browse files
committed
Add contextPath support to the grails-geb so it's easier to write tests in a multiproject environment
1 parent a70bc2d commit 7eae6e4

File tree

19 files changed

+637
-1
lines changed

19 files changed

+637
-1
lines changed

grails-doc/src/en/guide/upgrading/upgrading71x.adoc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,43 @@ Nested groups merge defaults — inner groups inherit and can extend or override
698698

699699
For full details, see the link:{guidePath}theWebLayer/urlmappings/mappingToControllersAndActions.html#_group_defaults[Group Defaults] documentation.
700700

701+
===== 2.10 ContainerGebSpec Context Path Support
702+
703+
`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.
704+
705+
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.
706+
707+
====== Example
708+
709+
[source,yaml]
710+
.application.yml
711+
----
712+
server:
713+
servlet:
714+
context-path: /myapp
715+
----
716+
717+
[source,groovy]
718+
----
719+
// Page URL is relative — no need to include /myapp
720+
class GreetingPage extends Page {
721+
static url = '/greeting'
722+
static at = { title == 'Greeting' }
723+
}
724+
725+
// Navigation automatically resolves to http://host:port/myapp/greeting
726+
@Integration
727+
class MySpec extends ContainerGebSpec {
728+
void 'should reach the greeting page'() {
729+
when:
730+
to(GreetingPage)
731+
732+
then:
733+
at(GreetingPage)
734+
}
735+
}
736+
----
737+
701738
===== 2.9 URL Mapping Wildcard Validation
702739

703740
Grails now validates wildcard-captured URL mapping variables (`$controller`, `$action`, `$namespace`) against registered controller artefacts at request time. When a captured value does not match a registered artefact, the mapping is skipped and the next mapping is tried.

grails-geb/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ This requires a [compatible container runtime](https://java.testcontainers.org/s
6868
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.
6969
Just run `./gradlew integrationTest` and a container will be started and configured to start a browser that can access your application under test.
7070

71+
#### Context Path Support
72+
73+
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:
74+
75+
```yaml
76+
# application.yml
77+
server:
78+
servlet:
79+
context-path: /myapp
80+
```
81+
82+
```groovy
83+
class GreetingPage extends Page {
84+
static url = '/greeting' // relative — resolves to /myapp/greeting
85+
static at = { title == 'Greeting' }
86+
}
87+
```
88+
7189
#### Parallel Execution
7290

7391
Parallel execution of `ContainerGebSpec` specifications is not currently supported.

grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import org.testcontainers.images.PullPolicy
5050
import org.testcontainers.utility.DockerImageName
5151

5252
import grails.plugin.geb.serviceloader.ServiceRegistry
53+
import grails.util.Holders
5354

5455
import static GrailsGebSettings.DEFAULT_AT_CHECK_WAITING
5556
import static GrailsGebSettings.DEFAULT_TIMEOUT_IMPLICITLY_WAIT
@@ -115,6 +116,15 @@ class WebDriverContainerHolder {
115116
}
116117
}
117118

119+
private static String findServerContextPath() {
120+
try {
121+
def applicationContext = Holders.findApplicationContext()
122+
return applicationContext?.environment?.getProperty('server.servlet.context-path', '/')
123+
} catch (ignored) {
124+
return '/'
125+
}
126+
}
127+
118128
@PackageScope
119129
boolean reinitialize(IMethodInvocation methodInvocation) {
120130
def specConf = new WebDriverContainerConfiguration(
@@ -285,8 +295,19 @@ class WebDriverContainerHolder {
285295
void setupBrowserUrl(IMethodInvocation methodInvocation) {
286296
if (!browser) return
287297
int hostPort = findServerPort(methodInvocation)
298+
String contextPath = findServerContextPath()
288299
Testcontainers.exposeHostPorts(hostPort)
289-
browser.baseUrl = "$containerConf.protocol://$containerConf.hostName:$hostPort"
300+
String baseUrl = "$containerConf.protocol://$containerConf.hostName:$hostPort"
301+
if (contextPath && contextPath != '/') {
302+
if (!contextPath.startsWith('/')) {
303+
contextPath = "/$contextPath"
304+
}
305+
if (!contextPath.endsWith('/')) {
306+
contextPath = "$contextPath/"
307+
}
308+
baseUrl += contextPath
309+
}
310+
browser.baseUrl = baseUrl
290311
}
291312

292313
private GebTestManager createTestManager() {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
plugins {
20+
id 'groovy'
21+
id 'org.apache.grails.buildsrc.properties'
22+
id 'org.apache.grails.buildsrc.compile'
23+
}
24+
25+
apply plugin: 'org.apache.grails.gradle.grails-web'
26+
apply plugin: 'org.apache.grails.gradle.grails-gsp'
27+
apply plugin: 'cloud.wondrify.asset-pipeline'
28+
29+
group = 'org.demo.contextpath'
30+
version = projectVersion
31+
32+
dependencies {
33+
34+
implementation platform(project(':grails-bom'))
35+
36+
implementation 'org.apache.grails:grails-core'
37+
implementation 'org.apache.grails:grails-logging'
38+
implementation 'org.apache.grails:grails-databinding'
39+
implementation 'org.apache.grails:grails-interceptors'
40+
implementation 'org.apache.grails:grails-rest-transforms'
41+
implementation 'org.apache.grails:grails-services'
42+
implementation 'org.apache.grails:grails-url-mappings'
43+
implementation 'org.apache.grails:grails-web-boot'
44+
implementation 'org.apache.grails:grails-gsp'
45+
if(System.getenv('SITEMESH3_TESTING_ENABLED') == 'true') {
46+
implementation 'org.apache.grails:grails-sitemesh3'
47+
}
48+
else {
49+
implementation 'org.apache.grails:grails-layout'
50+
}
51+
implementation 'org.apache.grails:grails-data-hibernate5'
52+
implementation 'org.springframework.boot:spring-boot-autoconfigure'
53+
implementation 'org.springframework.boot:spring-boot-starter'
54+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
55+
implementation 'org.springframework.boot:spring-boot-starter-logging'
56+
implementation 'org.springframework.boot:spring-boot-starter-tomcat'
57+
implementation 'org.springframework.boot:spring-boot-starter-validation'
58+
59+
testAndDevelopmentOnly platform(project(':grails-bom'))
60+
testAndDevelopmentOnly 'org.webjars.npm:bootstrap'
61+
testAndDevelopmentOnly 'org.webjars.npm:jquery'
62+
63+
runtimeOnly 'cloud.wondrify:asset-pipeline-grails'
64+
runtimeOnly 'com.h2database:h2'
65+
runtimeOnly 'org.apache.tomcat:tomcat-jdbc'
66+
runtimeOnly 'org.fusesource.jansi:jansi'
67+
68+
testImplementation 'org.apache.grails:grails-testing-support-datamapping'
69+
testImplementation 'org.apache.grails:grails-testing-support-web'
70+
testImplementation 'org.spockframework:spock-core'
71+
72+
integrationTestImplementation testFixtures('org.apache.grails:grails-geb')
73+
}
74+
75+
apply {
76+
from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
77+
from rootProject.layout.projectDirectory.file('gradle/test-webjar-asset-config.gradle')
78+
from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
79+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
server:
17+
servlet:
18+
context-path: /myapp
19+
info:
20+
app:
21+
name: '@info.app.name@'
22+
version: '@info.app.version@'
23+
grailsVersion: '@info.app.grailsVersion@'
24+
grails:
25+
views:
26+
default:
27+
codec: html
28+
gsp:
29+
encoding: UTF-8
30+
htmlcodec: xml
31+
codecs:
32+
expression: html
33+
scriptlet: html
34+
taglib: none
35+
staticparts: none
36+
mime:
37+
disable:
38+
accept:
39+
header:
40+
userAgents:
41+
- Gecko
42+
- WebKit
43+
- Presto
44+
- Trident
45+
types:
46+
all: '*/*'
47+
atom: application/atom+xml
48+
css: text/css
49+
csv: text/csv
50+
form: application/x-www-form-urlencoded
51+
html:
52+
- text/html
53+
- application/xhtml+xml
54+
js: text/javascript
55+
json:
56+
- application/json
57+
- text/json
58+
multipartForm: multipart/form-data
59+
pdf: application/pdf
60+
rss: application/rss+xml
61+
text: text/plain
62+
hal:
63+
- application/hal+json
64+
- application/hal+xml
65+
xml:
66+
- text/xml
67+
- application/xml
68+
codegen:
69+
defaultPackage: org.demo.contextpath
70+
profile: web
71+
dataSource:
72+
driverClassName: org.h2.Driver
73+
username: sa
74+
password: ''
75+
pooled: true
76+
jmxExport: true
77+
environments:
78+
development:
79+
dataSource:
80+
dbCreate: create-drop
81+
url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
82+
test:
83+
dataSource:
84+
dbCreate: update
85+
url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
86+
production:
87+
dataSource:
88+
dbCreate: none
89+
url: jdbc:h2:./prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
90+
hibernate:
91+
cache:
92+
queries: false
93+
use_second_level_cache: false
94+
use_query_cache: false
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.demo.contextpath
21+
22+
class GreetingController {
23+
24+
def index() {
25+
[message: 'Hello from Grails']
26+
}
27+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.demo.contextpath
21+
22+
class UrlMappings {
23+
static mappings = {
24+
"/$controller/$action?/$id?(.$format)?"{
25+
constraints {
26+
// apply constraints here
27+
}
28+
}
29+
30+
"/"(view:"/index")
31+
"500"(view:'/error')
32+
"404"(view:'/notFound')
33+
34+
}
35+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.demo.contextpath
21+
22+
import grails.boot.GrailsApp
23+
import grails.boot.config.GrailsAutoConfiguration
24+
import groovy.transform.CompileStatic
25+
26+
@CompileStatic
27+
class Application extends GrailsAutoConfiguration {
28+
static void main(String[] args) {
29+
GrailsApp.run(Application, args)
30+
}
31+
}

0 commit comments

Comments
 (0)