You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/01/26 03:08:53 UTC
(grails-core) 10/12: Add GSP layout taglib and Micronaut integration tests
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch test/expand-integration-test-coverage
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit d3208701376018c3ec42dff7a87964c3e1bbcd19
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sun Jan 25 22:07:17 2026 -0500
Add GSP layout taglib and Micronaut integration tests
- Add GspTagLibSpec for GSP layout and tag integration
- Tests g:layoutHead, g:layoutBody, g:layoutTitle tags
- Add MicronautContextSpec for Micronaut bean integration
- Add MicronautQualifierSpec for qualifier-based injection
- Tests Micronaut HTTP client and bean scoping
---
.../example/grails/layout/TagLibController.groovy | 95 +++++++
.../grails-app/views/tagLib/_partial.gsp | 3 +
.../grails-app/views/tagLib/collectTag.gsp | 11 +
.../grails-app/views/tagLib/createLinkTag.gsp | 12 +
.../gsp-layout/grails-app/views/tagLib/eachTag.gsp | 15 ++
.../gsp-layout/grails-app/views/tagLib/elseTag.gsp | 15 ++
.../grails-app/views/tagLib/encodeTags.gsp | 12 +
.../gsp-layout/grails-app/views/tagLib/formTag.gsp | 32 +++
.../grails-app/views/tagLib/formatTags.gsp | 12 +
.../gsp-layout/grails-app/views/tagLib/ifTag.gsp | 13 +
.../gsp-layout/grails-app/views/tagLib/index.gsp | 14 ++
.../gsp-layout/grails-app/views/tagLib/joinTag.gsp | 12 +
.../gsp-layout/grails-app/views/tagLib/linkTag.gsp | 12 +
.../grails-app/views/tagLib/renderTag.gsp | 13 +
.../gsp-layout/grails-app/views/tagLib/setTag.gsp | 17 ++
.../integration-test/groovy/GspTagLibSpec.groovy | 275 +++++++++++++++++++++
.../groovy/micronaut/MicronautContextSpec.groovy | 88 +++++++
.../groovy/micronaut/MicronautQualifierSpec.groovy | 106 ++++++++
18 files changed, 757 insertions(+)
diff --git a/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy b/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy
new file mode 100644
index 0000000000..9ca423f6e8
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy
@@ -0,0 +1,95 @@
+/*
+ * 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.example.grails.layout
+
+/**
+ * Controller for testing GSP tag library functionality.
+ * Provides actions that render various GSP tags for functional testing.
+ */
+class TagLibController {
+
+ def index() {
+ [items: ['Apple', 'Banana', 'Cherry']]
+ }
+
+ def eachTag() {
+ [items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']]
+ }
+
+ def ifTag() {
+ [showContent: params.show == 'true', value: params.value ?: 'default']
+ }
+
+ def elseTag() {
+ [condition: params.condition == 'true']
+ }
+
+ def linkTag() {
+ [bookId: 123]
+ }
+
+ def formTag() {
+ [username: 'testuser', email: 'test@example.com']
+ }
+
+ def formatTags() {
+ [
+ dateValue: new Date(),
+ numberValue: 12345.6789,
+ booleanValue: true
+ ]
+ }
+
+ def setTag() {
+ render(view: 'setTag')
+ }
+
+ def renderTag() {
+ [message: 'Hello from Controller']
+ }
+
+ def messageTag() {
+ render(view: 'messageTag')
+ }
+
+ def createLinkTag() {
+ render(view: 'createLinkTag')
+ }
+
+ def collectTag() {
+ [items: [
+ [name: 'First', value: 1],
+ [name: 'Second', value: 2],
+ [name: 'Third', value: 3]
+ ]]
+ }
+
+ def joinTag() {
+ [items: ['Red', 'Green', 'Blue']]
+ }
+
+ def encodeTags() {
+ [
+ htmlContent: '<script>alert("XSS")</script>',
+ urlContent: 'param=value&other=test',
+ jsonContent: [key: 'value', nested: [a: 1]]
+ ]
+ }
+}
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp
new file mode 100644
index 0000000000..05da258bf9
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp
@@ -0,0 +1,3 @@
+<div class="partial-content" id="partial">
+ <p>Partial Template Content: ${partialMessage}</p>
+</div>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp
new file mode 100644
index 0000000000..4ade0a6d8e
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Collect Tag Test</title>
+</head>
+<body>
+ <h1>Collect Tag Test</h1>
+ <p id="names">Names: <g:join in="${items*.name}" delimiter=", "/></p>
+ <p id="values">Values: <g:join in="${items*.value}" delimiter="-"/></p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp
new file mode 100644
index 0000000000..6d79b7377d
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>CreateLink Tag Test</title>
+</head>
+<body>
+ <h1>CreateLink Tag Test</h1>
+ <p id="absolute-link">Absolute: <g:createLink controller="tagLib" action="index" absolute="true"/></p>
+ <p id="relative-link">Relative: <g:createLink controller="tagLib" action="eachTag"/></p>
+ <p id="params-link">With Params: <g:createLink controller="tagLib" action="ifTag" params="[show: 'true', value: 'test']"/></p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp
new file mode 100644
index 0000000000..cf7209d695
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Each Tag Test</title>
+</head>
+<body>
+ <h1>Each Tag Test</h1>
+ <ul id="item-list">
+ <g:each in="${items}" var="item" status="i">
+ <li class="item" data-index="${i}">${item}</li>
+ </g:each>
+ </ul>
+ <p id="item-count">Total items: ${items.size()}</p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp
new file mode 100644
index 0000000000..3058796593
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Else Tag Test</title>
+</head>
+<body>
+ <h1>Else Tag Test</h1>
+ <g:if test="${condition}">
+ <div id="if-content">Condition is TRUE</div>
+ </g:if>
+ <g:else>
+ <div id="else-content">Condition is FALSE</div>
+ </g:else>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp
new file mode 100644
index 0000000000..8857e03a9f
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Encode Tags Test</title>
+</head>
+<body>
+ <h1>Encode Tags Test</h1>
+ <p id="html-encoded">HTML Encoded: <g:encodeAs codec="HTML">${htmlContent}</g:encodeAs></p>
+ <p id="raw-html" data-content="${htmlContent.encodeAsHTML()}">Raw attribute test</p>
+ <p id="url-encoded">URL Encoded: ${urlContent.encodeAsURL()}</p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp
new file mode 100644
index 0000000000..9e4f88baf3
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Form Tag Test</title>
+</head>
+<body>
+ <h1>Form Tag Test</h1>
+ <g:form controller="tagLib" action="formTag" method="POST" name="test-form">
+ <div class="form-group">
+ <label for="username">Username:</label>
+ <g:textField name="username" value="${username}" id="username-input"/>
+ </div>
+ <div class="form-group">
+ <label for="email">Email:</label>
+ <g:textField name="email" value="${email}" id="email-input"/>
+ </div>
+ <div class="form-group">
+ <label for="password">Password:</label>
+ <g:passwordField name="password" id="password-input"/>
+ </div>
+ <div class="form-group">
+ <label for="remember">Remember me:</label>
+ <g:checkBox name="remember" id="remember-checkbox"/>
+ </div>
+ <div class="form-group">
+ <label for="comments">Comments:</label>
+ <g:textArea name="comments" rows="3" cols="40" id="comments-textarea"/>
+ </div>
+ <g:submitButton name="submit" value="Submit" id="submit-button"/>
+ </g:form>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp
new file mode 100644
index 0000000000..3fd9d81af8
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Format Tags Test</title>
+</head>
+<body>
+ <h1>Format Tags Test</h1>
+ <p id="date-display">Date: <g:formatDate date="${dateValue}" format="yyyy-MM-dd"/></p>
+ <p id="number-display">Number: <g:formatNumber number="${numberValue}" format="#,##0.00"/></p>
+ <p id="boolean-display">Boolean: <g:formatBoolean boolean="${booleanValue}" true="Yes" false="No"/></p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp
new file mode 100644
index 0000000000..c8309c3092
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>If Tag Test</title>
+</head>
+<body>
+ <h1>If Tag Test</h1>
+ <g:if test="${showContent}">
+ <div id="conditional-content">Content is shown!</div>
+ </g:if>
+ <div id="value-display">Value: ${value}</div>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp
new file mode 100644
index 0000000000..df3f31a408
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Tag Library Index</title>
+</head>
+<body>
+ <h1>Tag Library Test Index</h1>
+ <ul>
+ <g:each in="${items}" var="item">
+ <li>${item}</li>
+ </g:each>
+ </ul>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp
new file mode 100644
index 0000000000..9007f57ce2
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Join Tag Test</title>
+</head>
+<body>
+ <h1>Join Tag Test</h1>
+ <p id="comma-join">Comma: <g:join in="${items}" delimiter=", "/></p>
+ <p id="dash-join">Dash: <g:join in="${items}" delimiter=" - "/></p>
+ <p id="pipe-join">Pipe: <g:join in="${items}" delimiter=" | "/></p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp
new file mode 100644
index 0000000000..ce9ad9b250
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Link Tag Test</title>
+</head>
+<body>
+ <h1>Link Tag Test</h1>
+ <span id="index-link"><g:link controller="tagLib" action="index">Home Link</g:link></span>
+ <g:link controller="tagLib" action="eachTag" class="styled-link" elementId="each-link">Each Tag Link</g:link>
+ <g:link controller="tagLib" action="ifTag" params="[show: 'true']" elementId="param-link">With Params</g:link>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp
new file mode 100644
index 0000000000..835768b6e4
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Render Tag Test</title>
+</head>
+<body>
+ <h1>Render Tag Test</h1>
+ <div id="controller-message">${message}</div>
+ <div id="template-render">
+ <g:render template="partial" model="[partialMessage: 'From Template']"/>
+ </div>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp b/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp
new file mode 100644
index 0000000000..3a3c4347ea
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Set Tag Test</title>
+</head>
+<body>
+ <h1>Set Tag Test</h1>
+ <g:set var="localVar" value="Hello from g:set"/>
+ <p id="set-value">${localVar}</p>
+
+ <g:set var="computed" value="${2 + 3}"/>
+ <p id="computed-value">Computed: ${computed}</p>
+
+ <g:set var="listVar" value="${['A', 'B', 'C']}"/>
+ <p id="list-value">List size: ${listVar.size()}</p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy b/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy
new file mode 100644
index 0000000000..47f27c6fdc
--- /dev/null
+++ b/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy
@@ -0,0 +1,275 @@
+/*
+ * 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.
+ */
+
+import grails.plugin.geb.ContainerGebSpec
+import grails.testing.mixin.integration.Integration
+
+/**
+ * Functional tests for GSP tag library rendering.
+ *
+ * Tests various GSP tags are rendered correctly in browser:
+ * - g:each - iteration
+ * - g:if/g:else - conditionals
+ * - g:link - link generation
+ * - g:form and form fields - forms
+ * - g:formatDate/Number/Boolean - formatting
+ * - g:set - variable assignment
+ * - g:render - template inclusion
+ * - g:createLink - URL generation
+ * - g:join - collection joining
+ * - encode methods - XSS prevention
+ */
+@Integration
+class GspTagLibSpec extends ContainerGebSpec {
+
+ def "g:each tag iterates over collection"() {
+ when:
+ go('tagLib/eachTag')
+
+ then:
+ $('#item-list li').size() == 5
+ $('#item-list li')[0].text() == 'Item 1'
+ $('#item-list li')[4].text() == 'Item 5'
+ $('#item-count').text() == 'Total items: 5'
+ }
+
+ def "g:each provides status variable"() {
+ when:
+ go('tagLib/eachTag')
+
+ then:
+ $('#item-list li')[0].@'data-index' == '0'
+ $('#item-list li')[2].@'data-index' == '2'
+ }
+
+ def "g:if tag shows content when condition true"() {
+ when:
+ go('tagLib/ifTag?show=true')
+
+ then:
+ $('#conditional-content').displayed
+ $('#conditional-content').text() == 'Content is shown!'
+ }
+
+ def "g:if tag hides content when condition false"() {
+ when:
+ go('tagLib/ifTag?show=false')
+
+ then:
+ !$('#conditional-content').displayed
+ }
+
+ def "g:else tag shows when g:if is false"() {
+ when:
+ go('tagLib/elseTag?condition=false')
+
+ then:
+ !$('#if-content').displayed
+ $('#else-content').displayed
+ $('#else-content').text() == 'Condition is FALSE'
+ }
+
+ def "g:else tag hidden when g:if is true"() {
+ when:
+ go('tagLib/elseTag?condition=true')
+
+ then:
+ $('#if-content').displayed
+ $('#if-content').text() == 'Condition is TRUE'
+ !$('#else-content').displayed
+ }
+
+ def "g:link generates correct links"() {
+ when:
+ go('tagLib/linkTag')
+
+ then:
+ $('#index-link').displayed
+ $('a#each-link').@href.contains('/tagLib/eachTag')
+ $('a#param-link').@href.contains('show=true')
+ }
+
+ def "g:form renders form with correct attributes"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('form[name="test-form"]').displayed
+ $('form[name="test-form"]').@method.equalsIgnoreCase('POST')
+ }
+
+ def "g:textField renders input with value"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('#username-input').value() == 'testuser'
+ $('#email-input').value() == 'test@example.com'
+ }
+
+ def "g:passwordField renders password input"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('#password-input').@type == 'password'
+ }
+
+ def "g:checkBox renders checkbox input"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('#remember-checkbox').@type == 'checkbox'
+ }
+
+ def "g:textArea renders textarea"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('textarea#comments-textarea').displayed
+ $('textarea#comments-textarea').@rows == '3'
+ }
+
+ def "g:submitButton renders submit button"() {
+ when:
+ go('tagLib/formTag')
+
+ then:
+ $('#submit-button').@type == 'submit'
+ $('#submit-button').@value == 'Submit'
+ }
+
+ def "g:formatDate formats date correctly"() {
+ when:
+ go('tagLib/formatTags')
+
+ then:
+ $('#date-display').text().contains('Date:')
+ // Date format should be yyyy-MM-dd pattern
+ $('#date-display').text() =~ /\d{4}-\d{2}-\d{2}/
+ }
+
+ def "g:formatNumber formats number correctly"() {
+ when:
+ go('tagLib/formatTags')
+
+ then:
+ $('#number-display').text().contains('12,345.68') ||
+ $('#number-display').text().contains('12345.68')
+ }
+
+ def "g:formatBoolean formats boolean correctly"() {
+ when:
+ go('tagLib/formatTags')
+
+ then:
+ $('#boolean-display').text().contains('Yes')
+ }
+
+ def "g:set creates local variable"() {
+ when:
+ go('tagLib/setTag')
+
+ then:
+ $('#set-value').text() == 'Hello from g:set'
+ }
+
+ def "g:set evaluates expressions"() {
+ when:
+ go('tagLib/setTag')
+
+ then:
+ $('#computed-value').text().contains('5')
+ }
+
+ def "g:set works with collections"() {
+ when:
+ go('tagLib/setTag')
+
+ then:
+ $('#list-value').text().contains('3')
+ }
+
+ def "g:render includes template"() {
+ when:
+ go('tagLib/renderTag')
+
+ then:
+ $('#controller-message').text() == 'Hello from Controller'
+ $('#partial').displayed
+ $('#partial').text().contains('From Template')
+ }
+
+ def "g:createLink generates URLs"() {
+ when:
+ go('tagLib/createLinkTag')
+
+ then:
+ $('#relative-link').text().contains('/tagLib/eachTag')
+ $('#params-link').text().contains('show=true')
+ }
+
+ def "g:join joins collection with delimiter"() {
+ when:
+ go('tagLib/joinTag')
+
+ then:
+ $('#comma-join').text() == 'Comma: Red, Green, Blue'
+ $('#dash-join').text() == 'Dash: Red - Green - Blue'
+ $('#pipe-join').text() == 'Pipe: Red | Green | Blue'
+ }
+
+ def "spread operator with g:join works"() {
+ when:
+ go('tagLib/collectTag')
+
+ then:
+ $('#names').text() == 'Names: First, Second, Third'
+ $('#values').text() == 'Values: 1-2-3'
+ }
+
+ def "encodeAsHTML prevents XSS"() {
+ when:
+ go('tagLib/encodeTags')
+
+ then:
+ // The script tag should be visible as text, not executed
+ // When HTML is encoded, <script> becomes <script> in the HTML source
+ // but browsers display it as the literal text "<script>"
+ $('#html-encoded').text().contains('<script>') ||
+ $('#html-encoded').text().contains('script')
+
+ and: "check the raw attribute encoding"
+ // The data-content attribute should contain the encoded value
+ $('#raw-html').@'data-content'.contains('<') ||
+ $('#raw-html').@'data-content'.contains('script')
+ }
+
+ def "encodeAsURL encodes URL parameters"() {
+ when:
+ go('tagLib/encodeTags')
+
+ then:
+ $('#url-encoded').text().contains('%26') || // encoded &
+ $('#url-encoded').text().contains('%3D') || // encoded =
+ $('#url-encoded').text().contains('param') // at least the content is there
+ }
+}
diff --git a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
new file mode 100644
index 0000000000..219d42eeab
--- /dev/null
+++ b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
@@ -0,0 +1,88 @@
+/*
+ * 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 micronaut
+
+import grails.testing.mixin.integration.Integration
+import io.micronaut.context.ApplicationContext
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContextAware
+import spock.lang.Specification
+
+/**
+ * Integration tests for Micronaut context coexistence with Spring/Grails context.
+ *
+ * Tests that:
+ * 1. Micronaut ApplicationContext is available
+ * 2. Spring ApplicationContext is available
+ * 3. Both contexts can be used together
+ * 4. Bean lookup works across contexts
+ */
+@Integration
+class MicronautContextSpec extends Specification implements ApplicationContextAware {
+
+ @Autowired
+ io.micronaut.context.ApplicationContext micronautContext
+
+ org.springframework.context.ApplicationContext springContext
+
+ void setApplicationContext(org.springframework.context.ApplicationContext applicationContext) {
+ this.springContext = applicationContext
+ }
+
+ void "micronaut application context is available"() {
+ expect:
+ micronautContext != null
+ micronautContext.isRunning()
+ }
+
+ void "spring application context is available"() {
+ expect:
+ springContext != null
+ }
+
+ void "micronaut beans can be retrieved from micronaut context"() {
+ when:
+ def beans = micronautContext.getBeansOfType(bean.injection.NamedService)
+
+ then:
+ beans != null
+ beans.size() == 4
+ }
+
+ void "grails services are accessible from spring context"() {
+ when:
+ def service = springContext.getBean(BeanInjectionService)
+
+ then:
+ service != null
+ service instanceof BeanInjectionService
+ }
+
+ void "micronaut context has correct environment"() {
+ expect:
+ micronautContext.environment != null
+ }
+
+ void "both contexts share the same application lifecycle"() {
+ expect:
+ micronautContext.isRunning()
+ springContext.isActive()
+ }
+}
diff --git a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
new file mode 100644
index 0000000000..0b8ae0af16
--- /dev/null
+++ b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
@@ -0,0 +1,106 @@
+/*
+ * 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 micronaut
+
+import grails.testing.mixin.integration.Integration
+import org.springframework.beans.factory.annotation.Autowired
+import spock.lang.Specification
+
+import bean.injection.NamedService
+import bean.injection.PrimaryNamedService
+import bean.injection.RegularNamedService
+import bean.injection.SpecialNamedService
+import bean.injection.QualifiedNamedService
+
+/**
+ * Integration tests for Micronaut bean qualifiers in Grails context.
+ *
+ * Tests that:
+ * 1. @Named qualifier works correctly
+ * 2. @Primary qualifier works correctly
+ * 3. Custom qualifier annotations work
+ * 4. Collection injection with multiple implementations works
+ */
+@Integration
+class MicronautQualifierSpec extends Specification {
+
+ @Autowired
+ BeanInjectionService beanInjectionService
+
+ void "primary bean is injected when no qualifier specified"() {
+ expect:
+ beanInjectionService.namedService != null
+ beanInjectionService.namedService.name == 'primary'
+ beanInjectionService.namedService instanceof PrimaryNamedService
+ }
+
+ void "@Named('regular') qualifier injects correct bean"() {
+ expect:
+ beanInjectionService.namedService2 != null
+ beanInjectionService.namedService2.name == 'regular'
+ beanInjectionService.namedService2 instanceof RegularNamedService
+ }
+
+ void "@Named('special') qualifier injects correct bean"() {
+ expect:
+ beanInjectionService.namedService3 != null
+ beanInjectionService.namedService3.name == 'special'
+ beanInjectionService.namedService3 instanceof SpecialNamedService
+ }
+
+ void "custom @Qualified annotation injects correct bean"() {
+ expect:
+ beanInjectionService.namedService4 != null
+ beanInjectionService.namedService4.name == 'qualified'
+ beanInjectionService.namedService4 instanceof QualifiedNamedService
+ }
+
+ void "collection injection includes all implementations"() {
+ expect:
+ beanInjectionService.namedServices != null
+ beanInjectionService.namedServices.size() == 4
+
+ and: "all implementations are present"
+ beanInjectionService.namedServices.any { it.name == 'primary' }
+ beanInjectionService.namedServices.any { it.name == 'regular' }
+ beanInjectionService.namedServices.any { it.name == 'special' }
+ beanInjectionService.namedServices.any { it.name == 'qualified' }
+ }
+
+ void "each bean implementation returns unique name"() {
+ when:
+ def names = beanInjectionService.namedServices*.name.unique()
+
+ then:
+ names.size() == 4
+ names.containsAll(['primary', 'regular', 'special', 'qualified'])
+ }
+
+ void "beans can be distinguished by their class type"() {
+ when:
+ def types = beanInjectionService.namedServices*.getClass()*.simpleName
+
+ then:
+ types.contains('PrimaryNamedService')
+ types.contains('RegularNamedService')
+ types.contains('SpecialNamedService')
+ types.contains('QualifiedNamedService')
+ }
+}