diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPageSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPageSpec.groovy new file mode 100644 index 00000000000..f439cde89f1 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GSPGrailsLayoutPageSpec.groovy @@ -0,0 +1,155 @@ +/* + * 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.apache.grails.web.layout + +import org.grails.buffer.StreamCharBuffer +import spock.lang.Specification + +class GSPGrailsLayoutPageSpec extends Specification { + + void 'empty page writes no content and is unused'() { + given: + def page = new GSPGrailsLayoutPage() + def writer = new StringWriter() + + when: + page.writeHead(writer) + page.writeBody(writer) + page.writePage(writer) + + then: + writer.toString() == '' + page.getHead() == null + page.getBody() == null + page.getPage() == null + page.getTitleBuffer() == null + page.getContentBuffer('page.sidebar') == null + !page.isUsed() + !page.isTitleCaptured() + } + + void 'captured buffers expose head body and original page separately'() { + given: + def page = new GSPGrailsLayoutPage(renderingLayout) + def original = 'headbody' + page.setPageBuffer(buffer(original)) + page.setHeadBuffer(buffer('head')) + page.setBodyBuffer(buffer('body')) + def head = new StringWriter() + def body = new StringWriter() + def full = new StringWriter() + + when: + page.writeHead(head) + page.writeBody(body) + page.writeOriginal(full) + + then: + page.isUsed() + page.getHead() == head.toString() + head.toString() == 'head' + page.getBody() == body.toString() + body.toString() == 'body' + page.getPage() == full.toString() + full.toString() == original + page.originalLength() == original.length() + + where: + renderingLayout << [false, true] + } + + void 'body output falls back to the whole page when no body has been captured'() { + given: + def page = new GSPGrailsLayoutPage() + page.setPageBuffer(buffer('fragment without a body tag')) + def writer = new StringWriter() + + when: + page.writeBody(writer) + + then: + writer.toString() == 'fragment without a body tag' + !page.isUsed() + } + + void 'captured title is removed from head output but its property is retained'() { + given: + def page = new GSPGrailsLayoutPage() + page.setHeadBuffer(buffer('Title')) + page.addProperty('title', 'Title') + page.setTitleCaptured(true) + def writer = new StringWriter() + + when: + page.writeHead(writer) + + then: + writer.toString() == '' + page.title == 'Title' + page.isTitleCaptured() + } + + void 'content and object properties mark the page as used'() { + given: + def page = new GSPGrailsLayoutPage() + def sidebar = buffer('Sidebar') + + when: + page.addProperty('page.count', Integer.valueOf(3)) + page.setContentBuffer('sidebar', sidebar) + + then: + page.isUsed() + page.getProperty('page.count') == '3' + page.getProperty('page.sidebar') == '' + page.getContentBuffer('page.sidebar').is(sidebar) + page.getContentBuffer('page.missing') == null + GSPGrailsLayoutPage.content2htmlPage(page).is(page) + } + + void 'reset clears captured content and flags so a page can be reused'() { + given: + def page = new GSPGrailsLayoutPage() + page.setHeadBuffer(buffer('head')) + page.setBodyBuffer(buffer('body')) + page.setPageBuffer(buffer('page')) + page.setTitleBuffer(buffer('title')) + page.setTitleCaptured(true) + page.setContentBuffer('sidebar', buffer('sidebar')) + + when: + page.reset() + + then: + !page.isUsed() + !page.isTitleCaptured() + page.getHead() == null + page.getBody() == null + page.getPage() == null + page.getTitleBuffer() == null + page.getContentBuffer('page.sidebar') == null + } + + private static StreamCharBuffer buffer(String text) { + def buffer = new StreamCharBuffer() + buffer.getWriter().write(text) + buffer + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsHTMLPageParserSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsHTMLPageParserSpec.groovy new file mode 100644 index 00000000000..c83e75e33bb --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsHTMLPageParserSpec.groovy @@ -0,0 +1,81 @@ +/* + * 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.apache.grails.web.layout + +import com.opensymphony.sitemesh.Content +import spock.lang.Specification + +class GrailsHTMLPageParserSpec extends Specification { + + void 'HTML parser extracts title head body and layout properties'() { + given: + def html = 'Title' + + '

Hello

' + + when: + def page = new GrailsHTMLPageParser().parse(html.toCharArray()) + + then: + page.title == 'Title' + page.head == '' + page.body == '

Hello

' + page.getProperty('lang') == 'en' + page.getProperty('body.class') == 'content' + page.getProperty('meta.layout') == 'main' + } + + void 'titles inside XML do not replace the HTML title'() { + given: + def html = 'Page title' + + 'Book titleBody' + + when: + def page = new GrailsHTMLPageParser().parse(html.toCharArray()) + + then: + page.title == 'Page title' + page.body.contains('Book title') + page.body.endsWith('Body') + } + + void 'parsed content exposes original head and body through the public Content interface'() { + given: + def html = 'TitleBody' + Content content = new GrailsHTMLPageParser().parseContent(html.toCharArray()) + def original = new StringWriter() + def head = new StringWriter() + def body = new StringWriter() + + when: + content.writeOriginal(original) + content.writeHead(head) + content.writeBody(body) + content.addProperty('page.extra', 'value') + + then: + original.toString() == html + content.originalLength() == html.length() + head.toString() == '' + body.toString() == 'Body' + content.title == 'Title' + content.getProperty('page.extra') == 'value' + GSPGrailsLayoutPage.content2htmlPage(content).body == 'Body' + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsLayoutTagLibSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsLayoutTagLibSpec.groovy new file mode 100644 index 00000000000..48420b3cd26 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsLayoutTagLibSpec.groovy @@ -0,0 +1,112 @@ +/* + * 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.apache.grails.web.layout + +import grails.testing.web.taglib.TagLibUnitTest +import org.grails.gsp.compiler.GrailsLayoutPreprocessor +import org.grails.plugins.web.taglib.GrailsLayoutTagLib +import spock.lang.Specification + +class GrailsLayoutTagLibSpec extends Specification implements TagLibUnitTest { + + GSPGrailsLayoutPage page + + void setup() { + page = new GSPGrailsLayoutPage() + request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, page) + } + + void 'capture tags render HTML and store head body and title on the page'() { + when: + def output = applyTemplate('Title' + + '' + + 'Body') + + then: + output == 'TitleBody' + page.title == 'Title' + page.isTitleCaptured() + page.head.contains('') + page.body == 'Body' + page.getProperty('body.class') == 'content' + page.isUsed() + } + + void 'wrapped title is captured in its own buffer so the layout head can drop it without reparsing'() { + when: + def output = applyTemplate('' + + 'Title' + + '') + def layoutHead = new StringWriter() + page.writeHead(layoutHead) + + then: + output == 'Title' + page.title == 'Title' + page.isTitleCaptured() + page.titleBuffer != null + layoutHead.toString() == '' + } + + void 'empty capture tags honour the preprocessor XML closing marker'() { + when: + def output = applyTemplate('') + + then: + output == '' + page.getProperty('meta.viewport') == 'width' + } + + void 'meta capture records both original and lowercase property names'() { + when: + def output = applyTemplate('' + + '') + + then: + output == '' + page.getProperty('meta.Description') == 'Details' + page.getProperty('meta.description') == 'Details' + page.getProperty('meta.http-equiv.Content-Type') == 'text/html' + page.getProperty('meta.http-equiv.content-type') == 'text/html' + } + + void 'parameter and content capture populate the page without writing the captured component'() { + when: + def output = applyTemplate('' + + 'Sidebar') + + then: + output == '' + page.getProperty('page.theme') == 'dark' + page.getContentBuffer('page.sidebar').toString() == 'Sidebar' + } + + void 'capture tags still render without a layout page'() { + given: + request.removeAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE) + + expect: + applyTemplate('Body') == + 'Body' + applyTemplate('Title' + + '') == 'Title' + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapperSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapperSpec.groovy new file mode 100644 index 00000000000..3e680a039d6 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsPageResponseWrapperSpec.groovy @@ -0,0 +1,223 @@ +/* + * 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.apache.grails.web.layout + +import com.opensymphony.module.sitemesh.PageParserSelector +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import spock.lang.Specification + +class GrailsPageResponseWrapperSpec extends Specification { + + MockHttpServletRequest request = new MockHttpServletRequest() + MockHttpServletResponse response = new MockHttpServletResponse() + PageParserSelector parserSelector = Stub(PageParserSelector) { + shouldParsePage('text/html') >> true + getPageParser('text/html') >> new GrailsHTMLPageParser() + } + + void 'HTML written through the writer is buffered and parsed without committing the response'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + def html = 'LayoutHello' + wrapper.setContentType('text/html;charset=UTF-8') + + when: + wrapper.getWriter().write(html) + wrapper.getWriter().flush() + wrapper.flushBuffer() + wrapper.setContentLength(html.length()) + wrapper.setHeader('Content-Length', '200') + wrapper.addHeader('content-length', '300') + wrapper.setHeader('X-Layout', 'buffered') + wrapper.addHeader('X-Layout', 'parsed') + + then: + wrapper.isGrailsLayoutActive() + !wrapper.isUsingStream() + new String(wrapper.getContents()) == html + wrapper.getPage().title == 'Layout' + wrapper.getPage().body == 'Hello' + response.contentAsString == '' + !response.committed + response.getHeader('Content-Length') == null + response.getHeaders('X-Layout') == ['buffered', 'parsed'] + } + + void 'HTML output stream uses the declared encoding'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + def html = 'caf\u00e9' + wrapper.setHeader('Content-Type', 'text/html;charset=UTF-8') + + when: + wrapper.getOutputStream().write(html.getBytes('UTF-8')) + def page = wrapper.getPage() + + then: + wrapper.isUsingStream() + page.page == html + page.body == 'caf\u00e9' + response.contentAsByteArray.length == 0 + } + + void 'buffering disallows switching between writer and output stream'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + wrapper.setContentType('text/html') + + when: + if (streamFirst) { + wrapper.getOutputStream().write(65) + wrapper.getWriter().write('B') + } else { + wrapper.getWriter().write('A') + wrapper.getOutputStream().write(66) + } + + then: + thrown(IllegalStateException) + + where: + streamFirst << [true, false] + } + + void 'unparseable content is passed through with content length and flush'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + + when: + wrapper.addHeader('Content-Type', 'text/plain') + wrapper.setContentLength(5) + wrapper.setHeader('Content-Length', '5') + wrapper.getWriter().write('plain') + wrapper.flushBuffer() + + then: + !wrapper.isGrailsLayoutActive() + wrapper.getPage() == null + wrapper.getContents() == null + response.contentAsString == 'plain' + response.getHeader('Content-Length') == '5' + response.committed + } + + void 'not modified status deactivates buffering and routes subsequent output to the target'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + wrapper.setContentType('text/html') + wrapper.getWriter().write('discarded') + + when: + wrapper.setStatus(304) + wrapper.getWriter().write('target') + wrapper.getWriter().flush() + + then: + response.status == 304 + !wrapper.isGrailsLayoutActive() + response.contentAsString == 'target' + } + + void 'error status clears buffered content before writing the error page'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + wrapper.setContentType('text/html') + wrapper.getWriter().write('discarded') + + when: + wrapper.setStatus(500) + wrapper.getWriter().write('Error') + + then: + response.status == 500 + wrapper.getPage().body == 'Error' + } + + void 'redirect and sendError discard buffered data and delegate status to the target'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + wrapper.setContentType('text/html') + wrapper.getWriter().write('discarded') + + when: + switch (operation) { + case 'redirect': + wrapper.sendRedirect('/next') + break + case 'error': + wrapper.sendError(404) + break + case 'message': + wrapper.sendError(503, 'Unavailable') + break + } + + then: + response.status == status + response.committed + response.redirectedUrl == location + response.errorMessage == message + wrapper.getContents() == null + + where: + operation | status | location | message + 'redirect' | 302 | '/next' | null + 'error' | 404 | null | null + 'message' | 503 | null | 'Unavailable' + } + + void 'captured GSP page is returned without reparsing and reset when layout is deactivated'() { + given: + def page = new GSPGrailsLayoutPage() + request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, page) + response.setContentType('text/html') + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + page.addProperty('title', 'Captured') + + expect: + wrapper.isGspGrailsLayoutActive() + wrapper.getPage().is(page) + + when: + wrapper.deactivateGrailsLayout() + + then: + !page.isUsed() + !wrapper.isGspGrailsLayoutActive() + wrapper.getPage() == null + } + + void 'include and forward dispatch metadata is delegated through the wrapper'() { + given: + def wrapper = new GrailsPageResponseWrapper(request, response, parserSelector) + + when: + request.getRequestDispatcher('/included').include(request, wrapper) + request.getRequestDispatcher('/forwarded').forward(request, wrapper) + wrapper.setStatus(201) + + then: + response.includedUrls == ['/included'] + response.forwardedUrl == '/forwarded' + wrapper.status == 201 + response.status == 201 + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponseSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponseSpec.groovy new file mode 100644 index 00000000000..2ee1fed5a75 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/GrailsViewBufferingResponseSpec.groovy @@ -0,0 +1,161 @@ +/* + * 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.apache.grails.web.layout + +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import spock.lang.Specification + +class GrailsViewBufferingResponseSpec extends Specification { + + void 'view response buffers HTML written as characters or bytes and builds Content'() { + given: + def request = new MockHttpServletRequest() + def target = new MockHttpServletResponse() + target.setContentType('text/html;charset=UTF-8') + def response = new GrailsViewBufferingResponse(request, target) + def html = 'Viewcaf\u00e9' + + when: + if (usingStream) { + response.getOutputStream().write(html.getBytes('UTF-8')) + } else { + response.getWriter().write(html) + } + def content = response.getContent() + def body = new StringWriter() + content.writeBody(body) + + then: + response.getTargetResponse().is(target) + response.isActive() + response.isUsingStream() == usingStream + content.title == 'View' + body.toString() == 'caf\u00e9' + target.contentAsByteArray.length == 0 + + where: + usingStream << [false, true] + } + + void 'non HTML responses pass through without producing Content'() { + given: + def target = new MockHttpServletResponse() + def response = new GrailsViewBufferingResponse(new MockHttpServletRequest(), target) + + when: + response.setContentType('application/json') + response.getWriter().write('{"value":1}') + response.getWriter().flush() + + then: + !response.isActive() + response.getContent() == null + target.contentAsString == '{"value":1}' + } + + void 'empty HTML writer has no Content to decorate'() { + given: + def response = new GrailsViewBufferingResponse(new MockHttpServletRequest(), new MockHttpServletResponse()) + response.setContentType('text/html') + response.getWriter().write('') + + expect: + response.isActive() + response.getContent() == null + } + + void 'GSP capture is reused and response mutator deactivation stops buffering'() { + given: + def request = new MockHttpServletRequest() + def page = new GSPGrailsLayoutPage() + request.setAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE, page) + def target = new MockHttpServletResponse() + def response = new GrailsViewBufferingResponse(request, target) + response.setContentType('text/html') + page.addProperty('title', 'Captured') + + expect: + response.getContent().is(page) + + when: + response.deactivateResponseMutator() + response.getWriter().write('passthrough') + response.getWriter().flush() + + then: + !response.isActive() + !page.isUsed() + response.getContent() == null + target.contentAsString == 'passthrough' + } + + void 'sendError after redirect does not replace the redirect status'() { + given: + def target = new MockHttpServletResponse() + def response = new GrailsViewBufferingResponse(new MockHttpServletRequest(), target) + + when: + response.sendRedirect('/next') + response.sendError(500) + response.sendError(503, 'Unavailable') + + then: + target.status == 302 + target.redirectedUrl == '/next' + target.errorMessage == null + } + + void 'sendError is skipped once the target response is committed'() { + given: + def target = new MockHttpServletResponse() + target.setCommitted(true) + def response = new GrailsViewBufferingResponse(new MockHttpServletRequest(), target) + + when: + response.sendError(500) + response.sendError(503, 'Unavailable') + + then: + target.status == 200 + target.errorMessage == null + } + + void 'sendError delegates status and message when the response is uncommitted'() { + given: + def target = new MockHttpServletResponse() + def response = new GrailsViewBufferingResponse(new MockHttpServletRequest(), target) + + when: + if (message == null) { + response.sendError(404) + } else { + response.sendError(404, message) + } + + then: + target.status == 404 + target.errorMessage == message + target.committed + + where: + message << [null, 'Missing'] + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/LayoutDecoratorSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/LayoutDecoratorSpec.groovy new file mode 100644 index 00000000000..74aab4da3c8 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/LayoutDecoratorSpec.groovy @@ -0,0 +1,125 @@ +/* + * 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.apache.grails.web.layout + +import com.opensymphony.module.sitemesh.RequestConstants +import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext +import org.grails.web.util.GrailsApplicationAttributes +import org.grails.web.util.WebUtils +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.web.servlet.View +import spock.lang.Specification + +class LayoutDecoratorSpec extends Specification { + + void 'no decorator writes the original page unchanged'() { + given: + def request = new MockHttpServletRequest() + def response = new MockHttpServletResponse() + def context = new SiteMeshWebAppContext(request, response, request.servletContext) + def html = 'OriginalOriginal body' + def content = new GrailsHTMLPageParser().parseContent(html.toCharArray()) + def decorator = new GrailsNoDecorator() + + when: + decorator.render(content, context) + + then: + response.contentAsString == html + decorator.getName() == null + decorator.getPage() == null + decorator.getURIPath() == null + decorator.getRole() == null + decorator.getInitParameter('missing') == null + decorator.getInitParameterNames() == null + } + + void 'view decorator makes page and model available during rendering and removes capture attributes afterwards'() { + given: + def request = new MockHttpServletRequest() + def response = new MockHttpServletResponse() + def content = new GrailsHTMLPageParser().parseContent('Body'.toCharArray()) + def model = [greeting: 'Hello'] + def view = Mock(View) + def decorator = new SpringMVCViewDecorator('main', view) + def pageDuringRender = null + def layoutPageDuringRender = null + + when: + decorator.render(content, model, request, response, request.servletContext) + + then: + 1 * view.render(model, request, response) >> { Map values, servletRequest, servletResponse -> + pageDuringRender = servletRequest.getAttribute(RequestConstants.PAGE) + layoutPageDuringRender = servletRequest.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE) + servletResponse.getWriter().write(values.greeting + ': Body') + } + pageDuringRender.body == 'Body' + layoutPageDuringRender instanceof GSPGrailsLayoutPage + response.contentAsString == 'Hello: Body' + response.committed + request.getAttribute(RequestConstants.PAGE) == null + request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE) == null + decorator.getView().is(view) + decorator.getName() == 'main' + decorator.getTemplate() == null + } + + void 'committed response skips layout rendering'() { + given: + def request = new MockHttpServletRequest() + def response = new MockHttpServletResponse() + response.setCommitted(true) + def view = Mock(View) + def decorator = new SpringMVCViewDecorator('main', view) + def context = new SiteMeshWebAppContext(request, response, request.servletContext) + + when: + decorator.render(new GSPGrailsLayoutPage(), context) + + then: + 0 * view.render(_, _, _) + request.getAttribute(RequestConstants.PAGE) == null + request.getAttribute(EmbeddedGrailsLayoutView.GSP_GRAILS_LAYOUT_PAGE) == null + } + + void 'render failure is wrapped with layout name and clears page scope and layout selection'() { + given: + def request = new MockHttpServletRequest() + def response = new MockHttpServletResponse() + request.setAttribute(GrailsApplicationAttributes.PAGE_SCOPE, 'scope') + request.setAttribute(WebUtils.LAYOUT_ATTRIBUTE, 'main') + def cause = new IllegalStateException('Cannot render') + def view = Mock(View) + def decorator = new SpringMVCViewDecorator('main', view) + + when: + decorator.render(new GSPGrailsLayoutPage(), [:], request, response, request.servletContext) + + then: + 1 * view.render(_, request, response) >> { throw cause } + def error = thrown(RuntimeException) + error.message == 'Error applying layout : main' + error.cause.is(cause) + request.getAttribute(GrailsApplicationAttributes.PAGE_SCOPE) == null + request.getAttribute(WebUtils.LAYOUT_ATTRIBUTE) == null + } +} diff --git a/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/RenderGrailsLayoutTagLibSpec.groovy b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/RenderGrailsLayoutTagLibSpec.groovy new file mode 100644 index 00000000000..86caa85a8f9 --- /dev/null +++ b/grails-gsp/grails-layout/src/test/groovy/org/apache/grails/web/layout/RenderGrailsLayoutTagLibSpec.groovy @@ -0,0 +1,102 @@ +/* + * 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.apache.grails.web.layout + +import com.opensymphony.module.sitemesh.RequestConstants +import grails.testing.web.taglib.TagLibUnitTest +import org.grails.buffer.FastStringWriter +import org.grails.plugins.web.taglib.RenderGrailsLayoutTagLib +import org.grails.taglib.GrailsTagException +import spock.lang.Specification + +class RenderGrailsLayoutTagLibSpec extends Specification implements TagLibUnitTest { + + GSPGrailsLayoutPage page + + void setup() { + page = new GSPGrailsLayoutPage() + request.setAttribute(RequestConstants.PAGE, page) + } + + void 'pageProperty renders a value, a default and an entire attribute'() { + given: + page.addProperty('body.class', 'content') + + expect: + applyTemplate('') == 'content' + applyTemplate('') == ' class="content"' + applyTemplate('') == 'fallback' + applyTemplate('') == '' + } + + void 'pageProperty requires a name attribute'() { + when: + applyTemplate('') + + then: + def error = thrown(GrailsTagException) + error.message.contains('missing required attribute [name]') + } + + void 'captured component buffers can be rendered and tested as page properties'() { + given: + def writer = new FastStringWriter() + writer.write('Sidebar') + page.setContentBuffer('sidebar', writer.getBuffer()) + + expect: + applyTemplate('') == 'Sidebar' + applyTemplate('Present') == 'Present' + } + + void 'ifPageProperty requires existing properties and matching values'() { + given: + page.addProperty('page.theme', 'dark') + page.addProperty('page.mode', 'wide') + + expect: + applyTemplate('Yes') == 'Yes' + applyTemplate('Hidden') == '' + applyTemplate('Hidden') == '' + applyTemplate('Hidden') == '' + applyTemplate('Both') == 'Both' + } + + void 'layout tags render captured title head and body with a default title when absent'() { + given: + def head = new FastStringWriter() + head.write('') + def body = new FastStringWriter() + body.write('Body') + page.setHeadBuffer(head.getBuffer()) + page.setBodyBuffer(body.getBuffer()) + + expect: + applyTemplate('||') == + 'Default||Body' + + when: + page.addProperty('title', 'Title') + + then: + applyTemplate('') == 'Title' + } +}