Skip to content

Commit 59fa0c7

Browse files
committed
build: thymeleaf unit tests
1 parent 15b49f2 commit 59fa0c7

5 files changed

Lines changed: 460 additions & 1 deletion

File tree

modules/jooby-thymeleaf/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,15 @@
4343
<artifactId>jooby-test</artifactId>
4444
<scope>test</scope>
4545
</dependency>
46+
<dependency>
47+
<groupId>org.mockito</groupId>
48+
<artifactId>mockito-core</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>org.mockito</groupId>
53+
<artifactId>mockito-junit-jupiter</artifactId>
54+
<scope>test</scope>
55+
</dependency>
4656
</dependencies>
4757
</project>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,54 @@
1+
/**
2+
* Thymeleaf module: https://jooby.io/modules/thymeleaf.
3+
*
4+
* <p>Usage:
5+
*
6+
* <pre>{@code
7+
* {
8+
*
9+
* install(new ThymeleafModule());
10+
*
11+
* get("/", ctx -> {
12+
* User user = ...;
13+
* return new ModelAndView("index.html")
14+
* .put("user", user);
15+
* });
16+
* }
17+
* }</pre>
18+
*
19+
* The template engine looks for a file-system directory: <code>views</code> in the current user
20+
* directory. If the directory doesn't exist, it looks for the same directory in the project
21+
* classpath.
22+
*
23+
* <p>Template engine supports the following file extensions: <code>.thl</code>, <code>.thl.html
24+
* </code> and <code>.html</code>.
25+
*
26+
* <p>You can specify a different template location:
27+
*
28+
* <pre>{@code
29+
* {
30+
*
31+
* install(new ThymeleafModule("mypath"));
32+
*
33+
* }
34+
* }</pre>
35+
*
36+
* The <code>mypath</code> location works in the same way: file-system or fallback to classpath.
37+
*
38+
* <p>Direct access to {@link org.thymeleaf.TemplateEngine} is available via require call:
39+
*
40+
* <pre>{@code
41+
* {
42+
*
43+
* TemplateEngine engine = require(TemplateEngine.class);
44+
*
45+
* }
46+
* }</pre>
47+
*
48+
* Complete documentation is available at: https://jooby.io/modules/thymeleaf.
49+
*
50+
* @author edgar
51+
* @since 2.0.0
52+
*/
153
@org.jspecify.annotations.NullMarked
254
package io.jooby.thymeleaf;

modules/jooby-thymeleaf/src/main/java/module-info.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,60 @@
44
* Copyright 2014 Edgar Espina
55
*/
66

7-
/** Thymeleaf module. */
7+
import org.thymeleaf.TemplateEngine;
8+
9+
/**
10+
* Thymeleaf module: https://jooby.io/modules/thymeleaf.
11+
*
12+
* <p>Usage:
13+
*
14+
* <pre>{@code
15+
* {
16+
*
17+
* install(new ThymeleafModule());
18+
*
19+
* get("/", ctx -> {
20+
* User user = ...;
21+
* return new ModelAndView("index.html")
22+
* .put("user", user);
23+
* });
24+
* }
25+
* }</pre>
26+
*
27+
* The template engine looks for a file-system directory: <code>views</code> in the current user
28+
* directory. If the directory doesn't exist, it looks for the same directory in the project
29+
* classpath.
30+
*
31+
* <p>Template engine supports the following file extensions: <code>.thl</code>, <code>.thl.html
32+
* </code> and <code>.html</code>.
33+
*
34+
* <p>You can specify a different template location:
35+
*
36+
* <pre>{@code
37+
* {
38+
*
39+
* install(new ThymeleafModule("mypath"));
40+
*
41+
* }
42+
* }</pre>
43+
*
44+
* The <code>mypath</code> location works in the same way: file-system or fallback to classpath.
45+
*
46+
* <p>Direct access to {@link TemplateEngine} is available via require call:
47+
*
48+
* <pre>{@code
49+
* {
50+
*
51+
* TemplateEngine engine = require(TemplateEngine.class);
52+
*
53+
* }
54+
* }</pre>
55+
*
56+
* Complete documentation is available at: https://jooby.io/modules/thymeleaf.
57+
*
58+
* @author edgar
59+
* @since 2.0.0
60+
*/
861
module io.jooby.thymeleaf {
962
exports io.jooby.thymeleaf;
1063

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.thymeleaf;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
import static org.mockito.ArgumentMatchers.eq;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.verify;
14+
import static org.mockito.Mockito.when;
15+
16+
import java.io.Writer;
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Locale;
20+
import java.util.Map;
21+
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.mockito.ArgumentCaptor;
26+
import org.mockito.Mock;
27+
import org.mockito.junit.jupiter.MockitoExtension;
28+
import org.thymeleaf.TemplateEngine;
29+
import org.thymeleaf.context.IContext;
30+
31+
import io.jooby.Context;
32+
import io.jooby.MapModelAndView;
33+
import io.jooby.ModelAndView;
34+
import io.jooby.output.BufferedOutput;
35+
import io.jooby.output.Output;
36+
import io.jooby.output.OutputFactory;
37+
38+
@ExtendWith(MockitoExtension.class)
39+
class ThymeleafTemplateEngineTest {
40+
41+
@Mock TemplateEngine templateEngine;
42+
@Mock Context ctx;
43+
@Mock OutputFactory outputFactory;
44+
@Mock BufferedOutput outputBuffer;
45+
@Mock Writer writer;
46+
47+
private ThymeleafTemplateEngine engine;
48+
49+
@BeforeEach
50+
void setup() {
51+
engine = new ThymeleafTemplateEngine(templateEngine, List.of(".thl", ".html"));
52+
}
53+
54+
@Test
55+
void testExtensions_AreAssignedAndUnmodifiable() {
56+
assertEquals(List.of(".thl", ".html"), engine.extensions());
57+
assertThrows(UnsupportedOperationException.class, () -> engine.extensions().add(".bad"));
58+
}
59+
60+
@Test
61+
void testRender_UnsupportedModelAndView() {
62+
ModelAndView<?> badModel = mock(ModelAndView.class);
63+
64+
assertThrows(ModelAndView.UnsupportedModelAndView.class, () -> engine.render(ctx, badModel));
65+
}
66+
67+
@Test
68+
void testRender_MapModelAndView_ViewWithoutSlash_LocaleFromContext() {
69+
MapModelAndView modelAndView = new MapModelAndView("index.html");
70+
modelAndView.put("user", "edgar");
71+
72+
Map<String, Object> ctxAttributes = new HashMap<>();
73+
ctxAttributes.put("flash", "success");
74+
75+
when(ctx.getAttributes()).thenReturn(ctxAttributes);
76+
when(ctx.locale()).thenReturn(Locale.UK);
77+
when(ctx.getOutputFactory()).thenReturn(outputFactory);
78+
when(outputFactory.allocate()).thenReturn(outputBuffer);
79+
when(outputBuffer.asWriter()).thenReturn(writer);
80+
81+
Output result = engine.render(ctx, modelAndView);
82+
83+
assertEquals(outputBuffer, result);
84+
85+
ArgumentCaptor<IContext> contextCaptor = ArgumentCaptor.forClass(IContext.class);
86+
87+
// Verifies the engine processed a sanitized path (prepended slash)
88+
verify(templateEngine).process(eq("/index.html"), contextCaptor.capture(), eq(writer));
89+
90+
IContext thymeleafContext = contextCaptor.getValue();
91+
92+
// Verifies the locale fell back to the context locale
93+
assertEquals(Locale.UK, thymeleafContext.getLocale());
94+
95+
// Verifies model attributes and context attributes were successfully merged
96+
assertTrue(thymeleafContext.containsVariable("user"));
97+
assertEquals("edgar", thymeleafContext.getVariable("user"));
98+
assertTrue(thymeleafContext.containsVariable("flash"));
99+
assertEquals("success", thymeleafContext.getVariable("flash"));
100+
}
101+
102+
@Test
103+
void testRender_MapModelAndView_ViewWithSlash_LocaleFromModel() {
104+
MapModelAndView modelAndView = new MapModelAndView("/admin/dashboard.html");
105+
modelAndView.setLocale(Locale.CANADA);
106+
107+
when(ctx.getAttributes()).thenReturn(new HashMap<>());
108+
when(ctx.getOutputFactory()).thenReturn(outputFactory);
109+
when(outputFactory.allocate()).thenReturn(outputBuffer);
110+
when(outputBuffer.asWriter()).thenReturn(writer);
111+
112+
engine.render(ctx, modelAndView);
113+
114+
ArgumentCaptor<IContext> contextCaptor = ArgumentCaptor.forClass(IContext.class);
115+
116+
// Verifies the engine processed the path as-is (slash already present)
117+
verify(templateEngine)
118+
.process(eq("/admin/dashboard.html"), contextCaptor.capture(), eq(writer));
119+
120+
IContext thymeleafContext = contextCaptor.getValue();
121+
122+
// Verifies the explicit model locale takes precedence
123+
assertEquals(Locale.CANADA, thymeleafContext.getLocale());
124+
}
125+
}

0 commit comments

Comments
 (0)