Created
April 9, 2016 02:56
-
-
Save danhyun/7d1a223e744f43721fe15f9837a24a5e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]--> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta name="generator" content="Asciidoctor 1.5.4-dev"> | |
<meta name="author" content="Dan Hyun"> | |
<title>Testing Ratpack Applications</title> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"> | |
<style> | |
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ | |
/* ========================================================================== HTML5 display definitions ========================================================================== */ | |
/** Correct `block` display not defined in IE 8/9. */ | |
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { | |
display: block; | |
} | |
/** Correct `inline-block` display not defined in IE 8/9. */ | |
audio, canvas, video { | |
display: inline-block; | |
} | |
/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ | |
audio:not([controls]) { | |
display: none; | |
height: 0; | |
} | |
/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ | |
[hidden], template { | |
display: none; | |
} | |
script { | |
display: none !important; | |
} | |
/* ========================================================================== Base ========================================================================== */ | |
/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ | |
html { | |
font-family: sans-serif; /* 1 */ | |
-ms-text-size-adjust: 100%; /* 2 */ | |
-webkit-text-size-adjust: 100%; /* 2 */ | |
} | |
/** Remove default margin. */ | |
body { | |
margin: 0; | |
} | |
/* ========================================================================== Links ========================================================================== */ | |
/** Remove the gray background color from active links in IE 10. */ | |
a { | |
background: transparent; | |
} | |
/** Address `outline` inconsistency between Chrome and other browsers. */ | |
a:focus { | |
outline: thin dotted; | |
} | |
/** Improve readability when focused and also mouse hovered in all browsers. */ | |
a:active, a:hover { | |
outline: 0; | |
} | |
/* ========================================================================== Typography ========================================================================== */ | |
/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ | |
h1 { | |
font-size: 2em; | |
margin: 0.67em 0; | |
} | |
/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ | |
abbr[title] { | |
border-bottom: 1px dotted; | |
} | |
/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ | |
b, strong { | |
font-weight: bold; | |
} | |
/** Address styling not present in Safari 5 and Chrome. */ | |
dfn { | |
font-style: italic; | |
} | |
/** Address differences between Firefox and other browsers. */ | |
hr { | |
-moz-box-sizing: content-box; | |
box-sizing: content-box; | |
height: 0; | |
} | |
/** Address styling not present in IE 8/9. */ | |
mark { | |
background: #ff0; | |
color: #000; | |
} | |
/** Correct font family set oddly in Safari 5 and Chrome. */ | |
code, kbd, pre, samp { | |
font-family: monospace, serif; | |
font-size: 1em; | |
} | |
/** Improve readability of pre-formatted text in all browsers. */ | |
pre { | |
white-space: pre-wrap; | |
} | |
/** Set consistent quote types. */ | |
q { | |
quotes: "\201C" "\201D" "\2018" "\2019"; | |
} | |
/** Address inconsistent and variable font size in all browsers. */ | |
small { | |
font-size: 80%; | |
} | |
/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ | |
sub, sup { | |
font-size: 75%; | |
line-height: 0; | |
position: relative; | |
vertical-align: baseline; | |
} | |
sup { | |
top: -0.5em; | |
} | |
sub { | |
bottom: -0.25em; | |
} | |
/* ========================================================================== Embedded content ========================================================================== */ | |
/** Remove border when inside `a` element in IE 8/9. */ | |
img { | |
border: 0; | |
} | |
/** Correct overflow displayed oddly in IE 9. */ | |
svg:not(:root) { | |
overflow: hidden; | |
} | |
/* ========================================================================== Figures ========================================================================== */ | |
/** Address margin not present in IE 8/9 and Safari 5. */ | |
figure { | |
margin: 0; | |
} | |
/* ========================================================================== Forms ========================================================================== */ | |
/** Define consistent border, margin, and padding. */ | |
fieldset { | |
border: 1px solid #c0c0c0; | |
margin: 0 2px; | |
padding: 0.35em 0.625em 0.75em; | |
} | |
/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ | |
legend { | |
border: 0; /* 1 */ | |
padding: 0; /* 2 */ | |
} | |
/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ | |
button, input, select, textarea { | |
font-family: inherit; /* 1 */ | |
font-size: 100%; /* 2 */ | |
margin: 0; /* 3 */ | |
} | |
/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ | |
button, input { | |
line-height: normal; | |
} | |
/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ | |
button, select { | |
text-transform: none; | |
} | |
/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ | |
button, html input[type="button"], input[type="reset"], input[type="submit"] { | |
-webkit-appearance: button; /* 2 */ | |
cursor: pointer; /* 3 */ | |
} | |
/** Re-set default cursor for disabled elements. */ | |
button[disabled], html input[disabled] { | |
cursor: default; | |
} | |
/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ | |
input[type="checkbox"], input[type="radio"] { | |
box-sizing: border-box; /* 1 */ | |
padding: 0; /* 2 */ | |
} | |
/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ | |
input[type="search"] { | |
-webkit-appearance: textfield; /* 1 */ | |
-moz-box-sizing: content-box; | |
-webkit-box-sizing: content-box; /* 2 */ | |
box-sizing: content-box; | |
} | |
/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ | |
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { | |
-webkit-appearance: none; | |
} | |
/** Remove inner padding and border in Firefox 4+. */ | |
button::-moz-focus-inner, input::-moz-focus-inner { | |
border: 0; | |
padding: 0; | |
} | |
/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ | |
textarea { | |
overflow: auto; /* 1 */ | |
vertical-align: top; /* 2 */ | |
} | |
/* ========================================================================== Tables ========================================================================== */ | |
/** Remove most spacing between table cells. */ | |
table { | |
border-collapse: collapse; | |
border-spacing: 0; | |
} | |
meta.foundation-mq-small { | |
font-family: "only screen and (min-width: 768px)"; | |
width: 768px; | |
} | |
meta.foundation-mq-medium { | |
font-family: "only screen and (min-width:1280px)"; | |
width: 1280px; | |
} | |
meta.foundation-mq-large { | |
font-family: "only screen and (min-width:1440px)"; | |
width: 1440px; | |
} | |
*, *:before, *:after { | |
-moz-box-sizing: border-box; | |
-webkit-box-sizing: border-box; | |
box-sizing: border-box; | |
} | |
html, body { | |
font-size: 100%; | |
} | |
body { | |
background: white; | |
color: rgba(0, 0, 0, 0.8); | |
padding: 0; | |
margin: 0; | |
font-family: "Noto Serif", "DejaVu Serif", serif; | |
font-weight: normal; | |
font-style: normal; | |
line-height: 1; | |
position: relative; | |
cursor: auto; | |
} | |
a:hover { | |
cursor: pointer; | |
} | |
img, object, embed { | |
max-width: 100%; | |
height: auto; | |
} | |
object, embed { | |
height: 100%; | |
} | |
img { | |
-ms-interpolation-mode: bicubic; | |
} | |
#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { | |
max-width: none !important; | |
} | |
.left { | |
float: left !important; | |
} | |
.right { | |
float: right !important; | |
} | |
.text-left { | |
text-align: left !important; | |
} | |
.text-right { | |
text-align: right !important; | |
} | |
.text-center { | |
text-align: center !important; | |
} | |
.text-justify { | |
text-align: justify !important; | |
} | |
.hide { | |
display: none; | |
} | |
.antialiased, body { | |
-webkit-font-smoothing: antialiased; | |
} | |
img { | |
display: inline-block; | |
vertical-align: middle; | |
} | |
textarea { | |
height: auto; | |
min-height: 50px; | |
} | |
select { | |
width: 100%; | |
} | |
p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { | |
font-size: 1.21875em; | |
line-height: 1.6; | |
} | |
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { | |
line-height: 1.45; | |
color: #7a2518; | |
font-weight: normal; | |
margin-top: 0; | |
margin-bottom: 0.25em; | |
} | |
/* Typography resets */ | |
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { | |
margin: 0; | |
padding: 0; | |
direction: ltr; | |
} | |
/* Default Link Styles */ | |
a { | |
color: #2156a5; | |
text-decoration: underline; | |
line-height: inherit; | |
} | |
a:hover, a:focus { | |
color: #1d4b8f; | |
} | |
a img { | |
border: none; | |
} | |
/* Default paragraph styles */ | |
p { | |
font-family: inherit; | |
font-weight: normal; | |
font-size: 1em; | |
line-height: 1.6; | |
margin-bottom: 1.25em; | |
text-rendering: optimizeLegibility; | |
} | |
p aside { | |
font-size: 0.875em; | |
line-height: 1.35; | |
font-style: italic; | |
} | |
/* Default header styles */ | |
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { | |
font-family: "Open Sans", "DejaVu Sans", sans-serif; | |
font-weight: 300; | |
font-style: normal; | |
color: #ba3925; | |
text-rendering: optimizeLegibility; | |
margin-top: 1em; | |
margin-bottom: 0.5em; | |
line-height: 1.0125em; | |
} | |
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { | |
font-size: 60%; | |
color: #e99b8f; | |
line-height: 0; | |
} | |
h1 { | |
font-size: 2.125em; | |
} | |
h2 { | |
font-size: 1.6875em; | |
} | |
h3, #toctitle, .sidebarblock > .content > .title { | |
font-size: 1.375em; | |
} | |
h4 { | |
font-size: 1.125em; | |
} | |
h5 { | |
font-size: 1.125em; | |
} | |
h6 { | |
font-size: 1em; | |
} | |
hr { | |
border: solid #ddddd8; | |
border-width: 1px 0 0; | |
clear: both; | |
margin: 1.25em 0 1.1875em; | |
height: 0; | |
} | |
/* Helpful Typography Defaults */ | |
em, i { | |
font-style: italic; | |
line-height: inherit; | |
} | |
strong, b { | |
font-weight: bold; | |
line-height: inherit; | |
} | |
small { | |
font-size: 60%; | |
line-height: inherit; | |
} | |
code { | |
font-family: "Droid Sans Mono", "DejaVu Sans Mono", "Monospace", monospace; | |
font-weight: normal; | |
color: rgba(0, 0, 0, 0.9); | |
} | |
/* Lists */ | |
ul, ol, dl { | |
font-size: 1em; | |
line-height: 1.6; | |
margin-bottom: 1.25em; | |
list-style-position: outside; | |
font-family: inherit; | |
} | |
ul, ol { | |
margin-left: 1.5em; | |
} | |
ul.no-bullet, ol.no-bullet { | |
margin-left: 1.5em; | |
} | |
/* Unordered Lists */ | |
ul li ul, ul li ol { | |
margin-left: 1.25em; | |
margin-bottom: 0; | |
font-size: 1em; /* Override nested font-size change */ | |
} | |
ul.square li ul, ul.circle li ul, ul.disc li ul { | |
list-style: inherit; | |
} | |
ul.square { | |
list-style-type: square; | |
} | |
ul.circle { | |
list-style-type: circle; | |
} | |
ul.disc { | |
list-style-type: disc; | |
} | |
ul.no-bullet { | |
list-style: none; | |
} | |
/* Ordered Lists */ | |
ol li ul, ol li ol { | |
margin-left: 1.25em; | |
margin-bottom: 0; | |
} | |
/* Definition Lists */ | |
dl dt { | |
margin-bottom: 0.3125em; | |
font-weight: bold; | |
} | |
dl dd { | |
margin-bottom: 1.25em; | |
} | |
/* Abbreviations */ | |
abbr, acronym { | |
text-transform: uppercase; | |
font-size: 90%; | |
color: rgba(0, 0, 0, 0.8); | |
border-bottom: 1px dotted #dddddd; | |
cursor: help; | |
} | |
abbr { | |
text-transform: none; | |
} | |
/* Blockquotes */ | |
blockquote { | |
margin: 0 0 1.25em; | |
padding: 0.5625em 1.25em 0 1.1875em; | |
border-left: 1px solid #dddddd; | |
} | |
blockquote cite { | |
display: block; | |
font-size: 0.9375em; | |
color: rgba(0, 0, 0, 0.6); | |
} | |
blockquote cite:before { | |
content: "\2014 \0020"; | |
} | |
blockquote cite a, blockquote cite a:visited { | |
color: rgba(0, 0, 0, 0.6); | |
} | |
blockquote, blockquote p { | |
line-height: 1.6; | |
color: rgba(0, 0, 0, 0.85); | |
} | |
/* Microformats */ | |
.vcard { | |
display: inline-block; | |
margin: 0 0 1.25em 0; | |
border: 1px solid #dddddd; | |
padding: 0.625em 0.75em; | |
} | |
.vcard li { | |
margin: 0; | |
display: block; | |
} | |
.vcard .fn { | |
font-weight: bold; | |
font-size: 0.9375em; | |
} | |
.vevent .summary { | |
font-weight: bold; | |
} | |
.vevent abbr { | |
cursor: auto; | |
text-decoration: none; | |
font-weight: bold; | |
border: none; | |
padding: 0 0.0625em; | |
} | |
@media only screen and (min-width: 768px) { | |
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { | |
line-height: 1.2; | |
} | |
h1 { | |
font-size: 2.75em; | |
} | |
h2 { | |
font-size: 2.3125em; | |
} | |
h3, #toctitle, .sidebarblock > .content > .title { | |
font-size: 1.6875em; | |
} | |
h4 { | |
font-size: 1.4375em; | |
} | |
} | |
/* Tables */ | |
table { | |
background: white; | |
margin-bottom: 1.25em; | |
border: solid 1px #dedede; | |
} | |
table thead, table tfoot { | |
background: #f7f8f7; | |
font-weight: bold; | |
} | |
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { | |
padding: 0.5em 0.625em 0.625em; | |
font-size: inherit; | |
color: rgba(0, 0, 0, 0.8); | |
text-align: left; | |
} | |
table tr th, table tr td { | |
padding: 0.5625em 0.625em; | |
font-size: inherit; | |
color: rgba(0, 0, 0, 0.8); | |
} | |
table tr.even, table tr.alt, table tr:nth-of-type(even) { | |
background: #f8f8f7; | |
} | |
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { | |
display: table-cell; | |
line-height: 1.6; | |
} | |
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { | |
line-height: 1.2; | |
word-spacing: -0.05em; | |
} | |
h1 strong, h2 strong, h3 strong, #toctitle strong, .sidebarblock > .content > .title strong, h4 strong, h5 strong, h6 strong { | |
font-weight: 400; | |
} | |
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { | |
content: " "; | |
display: table; | |
} | |
.clearfix:after, .float-group:after { | |
clear: both; | |
} | |
*:not(pre) > code { | |
font-size: 0.9375em; | |
font-style: normal !important; | |
letter-spacing: 0; | |
padding: 0.1em 0.5ex; | |
word-spacing: -0.15em; | |
background-color: #f7f7f8; | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
line-height: 1.45; | |
text-rendering: optimizeSpeed; | |
} | |
pre, pre > code { | |
line-height: 1.45; | |
color: rgba(0, 0, 0, 0.9); | |
font-family: "Droid Sans Mono", "DejaVu Sans Mono", "Monospace", monospace; | |
font-weight: normal; | |
text-rendering: optimizeSpeed; | |
} | |
.keyseq { | |
color: rgba(51, 51, 51, 0.8); | |
} | |
kbd { | |
display: inline-block; | |
color: rgba(0, 0, 0, 0.8); | |
font-size: 0.75em; | |
line-height: 1.4; | |
background-color: #f7f7f7; | |
border: 1px solid #ccc; | |
-webkit-border-radius: 3px; | |
border-radius: 3px; | |
-webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; | |
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; | |
margin: -0.15em 0.15em 0 0.15em; | |
padding: 0.2em 0.6em 0.2em 0.5em; | |
vertical-align: middle; | |
white-space: nowrap; | |
} | |
.keyseq kbd:first-child { | |
margin-left: 0; | |
} | |
.keyseq kbd:last-child { | |
margin-right: 0; | |
} | |
.menuseq, .menu { | |
color: rgba(0, 0, 0, 0.8); | |
} | |
b.button:before, b.button:after { | |
position: relative; | |
top: -1px; | |
font-weight: normal; | |
} | |
b.button:before { | |
content: "["; | |
padding: 0 3px 0 2px; | |
} | |
b.button:after { | |
content: "]"; | |
padding: 0 2px 0 3px; | |
} | |
p a > code:hover { | |
color: rgba(0, 0, 0, 0.9); | |
} | |
#header, #content, #footnotes, #footer { | |
width: 100%; | |
margin-left: auto; | |
margin-right: auto; | |
margin-top: 0; | |
margin-bottom: 0; | |
max-width: 62.5em; | |
*zoom: 1; | |
position: relative; | |
padding-left: 0.9375em; | |
padding-right: 0.9375em; | |
} | |
#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { | |
content: " "; | |
display: table; | |
} | |
#header:after, #content:after, #footnotes:after, #footer:after { | |
clear: both; | |
} | |
#content { | |
margin-top: 1.25em; | |
} | |
#content:before { | |
content: none; | |
} | |
#header > h1:first-child { | |
color: rgba(0, 0, 0, 0.85); | |
margin-top: 2.25rem; | |
margin-bottom: 0; | |
} | |
#header > h1:first-child + #toc { | |
margin-top: 8px; | |
border-top: 1px solid #ddddd8; | |
} | |
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { | |
border-bottom: 1px solid #ddddd8; | |
padding-bottom: 8px; | |
} | |
#header .details { | |
border-bottom: 1px solid #ddddd8; | |
line-height: 1.45; | |
padding-top: 0.25em; | |
padding-bottom: 0.25em; | |
padding-left: 0.25em; | |
color: rgba(0, 0, 0, 0.6); | |
display: -ms-flexbox; | |
display: -webkit-flex; | |
display: flex; | |
-ms-flex-flow: row wrap; | |
-webkit-flex-flow: row wrap; | |
flex-flow: row wrap; | |
} | |
#header .details span:first-child { | |
margin-left: -0.125em; | |
} | |
#header .details span.email a { | |
color: rgba(0, 0, 0, 0.85); | |
} | |
#header .details br { | |
display: none; | |
} | |
#header .details br + span:before { | |
content: "\00a0\2013\00a0"; | |
} | |
#header .details br + span.author:before { | |
content: "\00a0\22c5\00a0"; | |
color: rgba(0, 0, 0, 0.85); | |
} | |
#header .details br + span#revremark:before { | |
content: "\00a0|\00a0"; | |
} | |
#header #revnumber { | |
text-transform: capitalize; | |
} | |
#header #revnumber:after { | |
content: "\00a0"; | |
} | |
#content > h1:first-child:not([class]) { | |
color: rgba(0, 0, 0, 0.85); | |
border-bottom: 1px solid #ddddd8; | |
padding-bottom: 8px; | |
margin-top: 0; | |
padding-top: 1rem; | |
margin-bottom: 1.25rem; | |
} | |
#toc { | |
border-bottom: 1px solid #efefed; | |
padding-bottom: 0.5em; | |
} | |
#toc > ul { | |
margin-left: 0.125em; | |
} | |
#toc ul.sectlevel0 > li > a { | |
font-style: italic; | |
} | |
#toc ul.sectlevel0 ul.sectlevel1 { | |
margin: 0.5em 0; | |
} | |
#toc ul { | |
font-family: "Open Sans", "DejaVu Sans", sans-serif; | |
list-style-type: none; | |
} | |
#toc a { | |
text-decoration: none; | |
} | |
#toc a:active { | |
text-decoration: underline; | |
} | |
#toctitle { | |
color: #7a2518; | |
font-size: 1.2em; | |
} | |
@media only screen and (min-width: 768px) { | |
#toctitle { | |
font-size: 1.375em; | |
} | |
body.toc2 { | |
padding-left: 15em; | |
padding-right: 0; | |
} | |
#toc.toc2 { | |
margin-top: 0 !important; | |
background-color: #f8f8f7; | |
position: fixed; | |
width: 15em; | |
left: 0; | |
top: 0; | |
border-right: 1px solid #efefed; | |
border-top-width: 0 !important; | |
border-bottom-width: 0 !important; | |
z-index: 1000; | |
padding: 1.25em 1em; | |
height: 100%; | |
overflow: auto; | |
} | |
#toc.toc2 #toctitle { | |
margin-top: 0; | |
font-size: 1.2em; | |
} | |
#toc.toc2 > ul { | |
font-size: 0.9em; | |
margin-bottom: 0; | |
} | |
#toc.toc2 ul ul { | |
margin-left: 0; | |
padding-left: 1em; | |
} | |
#toc.toc2 ul.sectlevel0 ul.sectlevel1 { | |
padding-left: 0; | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
} | |
body.toc2.toc-right { | |
padding-left: 0; | |
padding-right: 15em; | |
} | |
body.toc2.toc-right #toc.toc2 { | |
border-right-width: 0; | |
border-left: 1px solid #efefed; | |
left: auto; | |
right: 0; | |
} | |
} | |
@media only screen and (min-width: 1280px) { | |
body.toc2 { | |
padding-left: 20em; | |
padding-right: 0; | |
} | |
#toc.toc2 { | |
width: 20em; | |
} | |
#toc.toc2 #toctitle { | |
font-size: 1.375em; | |
} | |
#toc.toc2 > ul { | |
font-size: 0.95em; | |
} | |
#toc.toc2 ul ul { | |
padding-left: 1.25em; | |
} | |
body.toc2.toc-right { | |
padding-left: 0; | |
padding-right: 20em; | |
} | |
} | |
#content #toc { | |
border-style: solid; | |
border-width: 1px; | |
border-color: #e0e0dc; | |
margin-bottom: 1.25em; | |
padding: 1.25em; | |
background: #f8f8f7; | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
} | |
#content #toc > :first-child { | |
margin-top: 0; | |
} | |
#content #toc > :last-child { | |
margin-bottom: 0; | |
} | |
#footer { | |
max-width: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
padding: 1.25em; | |
} | |
#footer-text { | |
color: rgba(255, 255, 255, 0.8); | |
line-height: 1.44; | |
} | |
.sect1 { | |
padding-bottom: 0.625em; | |
} | |
@media only screen and (min-width: 768px) { | |
.sect1 { | |
padding-bottom: 1.25em; | |
} | |
} | |
.sect1 + .sect1 { | |
border-top: 1px solid #efefed; | |
} | |
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { | |
position: absolute; | |
z-index: 1001; | |
width: 1.5ex; | |
margin-left: -1.5ex; | |
display: block; | |
text-decoration: none !important; | |
visibility: hidden; | |
text-align: center; | |
font-weight: normal; | |
} | |
#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { | |
content: "\00A7"; | |
font-size: 0.85em; | |
display: block; | |
padding-top: 0.1em; | |
} | |
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { | |
visibility: visible; | |
} | |
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { | |
color: #ba3925; | |
text-decoration: none; | |
} | |
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { | |
color: #a53221; | |
} | |
.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { | |
margin-bottom: 1.25em; | |
} | |
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { | |
text-rendering: optimizeLegibility; | |
text-align: left; | |
font-family: "Noto Serif", "DejaVu Serif", serif; | |
font-size: 1rem; | |
font-style: italic; | |
} | |
table.tableblock > caption.title { | |
white-space: nowrap; | |
overflow: visible; | |
max-width: 0; | |
} | |
.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { | |
color: rgba(0, 0, 0, 0.85); | |
} | |
table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { | |
font-size: inherit; | |
} | |
.admonitionblock > table { | |
border-collapse: separate; | |
border: 0; | |
background: none; | |
width: 100%; | |
} | |
.admonitionblock > table td.icon { | |
text-align: center; | |
width: 80px; | |
} | |
.admonitionblock > table td.icon img { | |
max-width: none; | |
} | |
.admonitionblock > table td.icon .title { | |
font-weight: bold; | |
font-family: "Open Sans", "DejaVu Sans", sans-serif; | |
text-transform: uppercase; | |
} | |
.admonitionblock > table td.content { | |
padding-left: 1.125em; | |
padding-right: 1.25em; | |
border-left: 1px solid #ddddd8; | |
color: rgba(0, 0, 0, 0.6); | |
} | |
.admonitionblock > table td.content > :last-child > :last-child { | |
margin-bottom: 0; | |
} | |
.exampleblock > .content { | |
border-style: solid; | |
border-width: 1px; | |
border-color: #e6e6e6; | |
margin-bottom: 1.25em; | |
padding: 1.25em; | |
background: white; | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
} | |
.exampleblock > .content > :first-child { | |
margin-top: 0; | |
} | |
.exampleblock > .content > :last-child { | |
margin-bottom: 0; | |
} | |
.sidebarblock { | |
border-style: solid; | |
border-width: 1px; | |
border-color: #e0e0dc; | |
margin-bottom: 1.25em; | |
padding: 1.25em; | |
background: #f8f8f7; | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
} | |
.sidebarblock > :first-child { | |
margin-top: 0; | |
} | |
.sidebarblock > :last-child { | |
margin-bottom: 0; | |
} | |
.sidebarblock > .content > .title { | |
color: #7a2518; | |
margin-top: 0; | |
text-align: center; | |
} | |
.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { | |
margin-bottom: 0; | |
} | |
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { | |
background: #f7f7f8; | |
} | |
.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { | |
background: #f2f1f1; | |
} | |
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
word-wrap: break-word; | |
padding: 1em; | |
font-size: 0.8125em; | |
} | |
.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { | |
overflow-x: auto; | |
white-space: pre; | |
word-wrap: normal; | |
} | |
@media only screen and (min-width: 768px) { | |
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { | |
font-size: 0.90625em; | |
} | |
} | |
@media only screen and (min-width: 1280px) { | |
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { | |
font-size: 1em; | |
} | |
} | |
.literalblock.output pre { | |
color: #f7f7f8; | |
background-color: rgba(0, 0, 0, 0.9); | |
} | |
.listingblock pre.highlightjs { | |
padding: 0; | |
} | |
.listingblock pre.highlightjs > code { | |
padding: 1em; | |
-webkit-border-radius: 4px; | |
border-radius: 4px; | |
} | |
.listingblock pre.prettyprint { | |
border-width: 0; | |
} | |
.listingblock > .content { | |
position: relative; | |
} | |
.listingblock code[data-lang]:before { | |
display: none; | |
content: attr(data-lang); | |
position: absolute; | |
font-size: 0.75em; | |
top: 0.425rem; | |
right: 0.5rem; | |
line-height: 1; | |
text-transform: uppercase; | |
color: #999; | |
} | |
.listingblock:hover code[data-lang]:before { | |
display: block; | |
} | |
.listingblock.terminal pre .command:before { | |
content: attr(data-prompt); | |
padding-right: 0.5em; | |
color: #999; | |
} | |
.listingblock.terminal pre .command:not([data-prompt]):before { | |
content: "$"; | |
} | |
table.pyhltable { | |
border-collapse: separate; | |
border: 0; | |
margin-bottom: 0; | |
background: none; | |
} | |
table.pyhltable td { | |
vertical-align: top; | |
padding-top: 0; | |
padding-bottom: 0; | |
} | |
table.pyhltable td.code { | |
padding-left: .75em; | |
padding-right: 0; | |
} | |
pre.pygments .lineno, table.pyhltable td:not(.code) { | |
color: #999; | |
padding-left: 0; | |
padding-right: .5em; | |
border-right: 1px solid #ddddd8; | |
} | |
pre.pygments .lineno { | |
display: inline-block; | |
margin-right: .25em; | |
} | |
table.pyhltable .linenodiv { | |
background: none !important; | |
padding-right: 0 !important; | |
} | |
.quoteblock { | |
margin: 0 1em 1.25em 1.5em; | |
display: table; | |
} | |
.quoteblock > .title { | |
margin-left: -1.5em; | |
margin-bottom: 0.75em; | |
} | |
.quoteblock blockquote, .quoteblock blockquote p { | |
color: rgba(0, 0, 0, 0.85); | |
font-size: 1.15rem; | |
line-height: 1.75; | |
word-spacing: 0.1em; | |
letter-spacing: 0; | |
font-style: italic; | |
text-align: justify; | |
} | |
.quoteblock blockquote { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
} | |
.quoteblock blockquote:before { | |
content: "\201c"; | |
float: left; | |
font-size: 2.75em; | |
font-weight: bold; | |
line-height: 0.6em; | |
margin-left: -0.6em; | |
color: #7a2518; | |
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); | |
} | |
.quoteblock blockquote > .paragraph:last-child p { | |
margin-bottom: 0; | |
} | |
.quoteblock .attribution { | |
margin-top: 0.5em; | |
margin-right: 0.5ex; | |
text-align: right; | |
} | |
.quoteblock .quoteblock { | |
margin-left: 0; | |
margin-right: 0; | |
padding: 0.5em 0; | |
border-left: 3px solid rgba(0, 0, 0, 0.6); | |
} | |
.quoteblock .quoteblock blockquote { | |
padding: 0 0 0 0.75em; | |
} | |
.quoteblock .quoteblock blockquote:before { | |
display: none; | |
} | |
.verseblock { | |
margin: 0 1em 1.25em 1em; | |
} | |
.verseblock pre { | |
font-family: "Open Sans", "DejaVu Sans", sans; | |
font-size: 1.15rem; | |
color: rgba(0, 0, 0, 0.85); | |
font-weight: 300; | |
text-rendering: optimizeLegibility; | |
} | |
.verseblock pre strong { | |
font-weight: 400; | |
} | |
.verseblock .attribution { | |
margin-top: 1.25rem; | |
margin-left: 0.5ex; | |
} | |
.quoteblock .attribution, .verseblock .attribution { | |
font-size: 0.9375em; | |
line-height: 1.45; | |
font-style: italic; | |
} | |
.quoteblock .attribution br, .verseblock .attribution br { | |
display: none; | |
} | |
.quoteblock .attribution cite, .verseblock .attribution cite { | |
display: block; | |
letter-spacing: -0.05em; | |
color: rgba(0, 0, 0, 0.6); | |
} | |
.quoteblock.abstract { | |
margin: 0 0 1.25em 0; | |
display: block; | |
} | |
.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { | |
text-align: left; | |
word-spacing: 0; | |
} | |
.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { | |
display: none; | |
} | |
table.tableblock { | |
max-width: 100%; | |
border-collapse: separate; | |
} | |
table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { | |
margin-bottom: 0; | |
} | |
table.spread { | |
width: 100%; | |
} | |
table.tableblock, th.tableblock, td.tableblock { | |
border: 0 solid #dedede; | |
} | |
table.grid-all th.tableblock, table.grid-all td.tableblock { | |
border-width: 0 1px 1px 0; | |
} | |
table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { | |
border-width: 1px 1px 0 0; | |
} | |
table.grid-cols th.tableblock, table.grid-cols td.tableblock { | |
border-width: 0 1px 0 0; | |
} | |
table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { | |
border-right-width: 0; | |
} | |
table.grid-rows th.tableblock, table.grid-rows td.tableblock { | |
border-width: 0 0 1px 0; | |
} | |
table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { | |
border-bottom-width: 0; | |
} | |
table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { | |
border-width: 1px 0 0 0; | |
} | |
table.frame-all { | |
border-width: 1px; | |
} | |
table.frame-sides { | |
border-width: 0 1px; | |
} | |
table.frame-topbot { | |
border-width: 1px 0; | |
} | |
th.halign-left, td.halign-left { | |
text-align: left; | |
} | |
th.halign-right, td.halign-right { | |
text-align: right; | |
} | |
th.halign-center, td.halign-center { | |
text-align: center; | |
} | |
th.valign-top, td.valign-top { | |
vertical-align: top; | |
} | |
th.valign-bottom, td.valign-bottom { | |
vertical-align: bottom; | |
} | |
th.valign-middle, td.valign-middle { | |
vertical-align: middle; | |
} | |
table thead th, table tfoot th { | |
font-weight: bold; | |
} | |
tbody tr th { | |
display: table-cell; | |
line-height: 1.6; | |
background: #f7f8f7; | |
} | |
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { | |
color: rgba(0, 0, 0, 0.8); | |
font-weight: bold; | |
} | |
p.tableblock > code:only-child { | |
background: none; | |
padding: 0; | |
} | |
p.tableblock { | |
font-size: 1em; | |
} | |
td > div.verse { | |
white-space: pre; | |
} | |
ol { | |
margin-left: 1.75em; | |
} | |
ul li ol { | |
margin-left: 1.5em; | |
} | |
dl dd { | |
margin-left: 1.125em; | |
} | |
dl dd:last-child, dl dd:last-child > :last-child { | |
margin-bottom: 0; | |
} | |
ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { | |
margin-bottom: 0.625em; | |
} | |
ul.unstyled, ol.unnumbered, ul.checklist, ul.none { | |
list-style-type: none; | |
} | |
ul.unstyled, ol.unnumbered, ul.checklist { | |
margin-left: 0.625em; | |
} | |
ul.checklist li > p:first-child > .fa-check-square-o:first-child, ul.checklist li > p:first-child > input[type="checkbox"]:first-child { | |
margin-right: 0.25em; | |
} | |
ul.checklist li > p:first-child > input[type="checkbox"]:first-child { | |
position: relative; | |
top: 1px; | |
} | |
ul.inline { | |
margin: 0 auto 0.625em auto; | |
margin-left: -1.375em; | |
margin-right: 0; | |
padding: 0; | |
list-style: none; | |
overflow: hidden; | |
} | |
ul.inline > li { | |
list-style: none; | |
float: left; | |
margin-left: 1.375em; | |
display: block; | |
} | |
ul.inline > li > * { | |
display: block; | |
} | |
.unstyled dl dt { | |
font-weight: normal; | |
font-style: normal; | |
} | |
ol.arabic { | |
list-style-type: decimal; | |
} | |
ol.decimal { | |
list-style-type: decimal-leading-zero; | |
} | |
ol.loweralpha { | |
list-style-type: lower-alpha; | |
} | |
ol.upperalpha { | |
list-style-type: upper-alpha; | |
} | |
ol.lowerroman { | |
list-style-type: lower-roman; | |
} | |
ol.upperroman { | |
list-style-type: upper-roman; | |
} | |
ol.lowergreek { | |
list-style-type: lower-greek; | |
} | |
.hdlist > table, .colist > table { | |
border: 0; | |
background: none; | |
} | |
.hdlist > table > tbody > tr, .colist > table > tbody > tr { | |
background: none; | |
} | |
td.hdlist1 { | |
padding-right: .75em; | |
font-weight: bold; | |
} | |
td.hdlist1, td.hdlist2 { | |
vertical-align: top; | |
} | |
.literalblock + .colist, .listingblock + .colist { | |
margin-top: -0.5em; | |
} | |
.colist > table tr > td:first-of-type { | |
padding: 0 0.75em; | |
line-height: 1; | |
} | |
.colist > table tr > td:last-of-type { | |
padding: 0.25em 0; | |
} | |
.thumb, .th { | |
line-height: 0; | |
display: inline-block; | |
border: solid 4px white; | |
-webkit-box-shadow: 0 0 0 1px #dddddd; | |
box-shadow: 0 0 0 1px #dddddd; | |
} | |
.imageblock.left, .imageblock[style*="float: left"] { | |
margin: 0.25em 0.625em 1.25em 0; | |
} | |
.imageblock.right, .imageblock[style*="float: right"] { | |
margin: 0.25em 0 1.25em 0.625em; | |
} | |
.imageblock > .title { | |
margin-bottom: 0; | |
} | |
.imageblock.thumb, .imageblock.th { | |
border-width: 6px; | |
} | |
.imageblock.thumb > .title, .imageblock.th > .title { | |
padding: 0 0.125em; | |
} | |
.image.left, .image.right { | |
margin-top: 0.25em; | |
margin-bottom: 0.25em; | |
display: inline-block; | |
line-height: 0; | |
} | |
.image.left { | |
margin-right: 0.625em; | |
} | |
.image.right { | |
margin-left: 0.625em; | |
} | |
a.image { | |
text-decoration: none; | |
} | |
span.footnote, span.footnoteref { | |
vertical-align: super; | |
font-size: 0.875em; | |
} | |
span.footnote a, span.footnoteref a { | |
text-decoration: none; | |
} | |
span.footnote a:active, span.footnoteref a:active { | |
text-decoration: underline; | |
} | |
#footnotes { | |
padding-top: 0.75em; | |
padding-bottom: 0.75em; | |
margin-bottom: 0.625em; | |
} | |
#footnotes hr { | |
width: 20%; | |
min-width: 6.25em; | |
margin: -.25em 0 .75em 0; | |
border-width: 1px 0 0 0; | |
} | |
#footnotes .footnote { | |
padding: 0 0.375em; | |
line-height: 1.3; | |
font-size: 0.875em; | |
margin-left: 1.2em; | |
text-indent: -1.2em; | |
margin-bottom: .2em; | |
} | |
#footnotes .footnote a:first-of-type { | |
font-weight: bold; | |
text-decoration: none; | |
} | |
#footnotes .footnote:last-of-type { | |
margin-bottom: 0; | |
} | |
#content #footnotes { | |
margin-top: -0.625em; | |
margin-bottom: 0; | |
padding: 0.75em 0; | |
} | |
.gist .file-data > table { | |
border: 0; | |
background: #fff; | |
width: 100%; | |
margin-bottom: 0; | |
} | |
.gist .file-data > table td.line-data { | |
width: 99%; | |
} | |
div.unbreakable { | |
page-break-inside: avoid; | |
} | |
.big { | |
font-size: larger; | |
} | |
.small { | |
font-size: smaller; | |
} | |
.underline { | |
text-decoration: underline; | |
} | |
.overline { | |
text-decoration: overline; | |
} | |
.line-through { | |
text-decoration: line-through; | |
} | |
.aqua { | |
color: #00bfbf; | |
} | |
.aqua-background { | |
background-color: #00fafa; | |
} | |
.black { | |
color: black; | |
} | |
.black-background { | |
background-color: black; | |
} | |
.blue { | |
color: #0000bf; | |
} | |
.blue-background { | |
background-color: #0000fa; | |
} | |
.fuchsia { | |
color: #bf00bf; | |
} | |
.fuchsia-background { | |
background-color: #fa00fa; | |
} | |
.gray { | |
color: #606060; | |
} | |
.gray-background { | |
background-color: #7d7d7d; | |
} | |
.green { | |
color: #006000; | |
} | |
.green-background { | |
background-color: #007d00; | |
} | |
.lime { | |
color: #00bf00; | |
} | |
.lime-background { | |
background-color: #00fa00; | |
} | |
.maroon { | |
color: #600000; | |
} | |
.maroon-background { | |
background-color: #7d0000; | |
} | |
.navy { | |
color: #000060; | |
} | |
.navy-background { | |
background-color: #00007d; | |
} | |
.olive { | |
color: #606000; | |
} | |
.olive-background { | |
background-color: #7d7d00; | |
} | |
.purple { | |
color: #600060; | |
} | |
.purple-background { | |
background-color: #7d007d; | |
} | |
.red { | |
color: #bf0000; | |
} | |
.red-background { | |
background-color: #fa0000; | |
} | |
.silver { | |
color: #909090; | |
} | |
.silver-background { | |
background-color: #bcbcbc; | |
} | |
.teal { | |
color: #006060; | |
} | |
.teal-background { | |
background-color: #007d7d; | |
} | |
.white { | |
color: #bfbfbf; | |
} | |
.white-background { | |
background-color: #fafafa; | |
} | |
.yellow { | |
color: #bfbf00; | |
} | |
.yellow-background { | |
background-color: #fafa00; | |
} | |
span.icon > .fa { | |
cursor: default; | |
} | |
.admonitionblock td.icon [class^="fa icon-"] { | |
font-size: 2.5em; | |
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); | |
cursor: default; | |
} | |
.admonitionblock td.icon .icon-note:before { | |
content: "\f05a"; | |
color: #19407c; | |
} | |
.admonitionblock td.icon .icon-tip:before { | |
content: "\f0eb"; | |
text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); | |
color: #111; | |
} | |
.admonitionblock td.icon .icon-warning:before { | |
content: "\f071"; | |
color: #bf6900; | |
} | |
.admonitionblock td.icon .icon-caution:before { | |
content: "\f06d"; | |
color: #bf3400; | |
} | |
.admonitionblock td.icon .icon-important:before { | |
content: "\f06a"; | |
color: #bf0000; | |
} | |
.conum[data-value] { | |
display: inline-block; | |
color: #fff !important; | |
background-color: rgba(0, 0, 0, 0.8); | |
-webkit-border-radius: 100px; | |
border-radius: 100px; | |
text-align: center; | |
font-size: 0.75em; | |
width: 1.67em; | |
height: 1.67em; | |
line-height: 1.67em; | |
font-family: "Open Sans", "DejaVu Sans", sans-serif; | |
font-style: normal; | |
font-weight: bold; | |
} | |
.conum[data-value] * { | |
color: #fff !important; | |
} | |
.conum[data-value] + b { | |
display: none; | |
} | |
.conum[data-value]:after { | |
content: attr(data-value); | |
} | |
pre .conum[data-value] { | |
position: relative; | |
top: -0.125em; | |
} | |
b.conum * { | |
color: inherit !important; | |
} | |
.conum:not([data-value]):empty { | |
display: none; | |
} | |
h1, h2 { | |
letter-spacing: -0.01em; | |
} | |
dt, th.tableblock, td.content { | |
text-rendering: optimizeLegibility; | |
} | |
p, td.content { | |
letter-spacing: -0.01em; | |
} | |
p strong, td.content strong { | |
letter-spacing: -0.005em; | |
} | |
p, blockquote, dt, td.content { | |
font-size: 1.0625rem; | |
} | |
p { | |
margin-bottom: 1.25rem; | |
} | |
.sidebarblock p, .sidebarblock dt, .sidebarblock td.content, p.tableblock { | |
font-size: 1em; | |
} | |
.exampleblock > .content { | |
background-color: #fffef7; | |
border-color: #e0e0dc; | |
-webkit-box-shadow: 0 1px 4px #e0e0dc; | |
box-shadow: 0 1px 4px #e0e0dc; | |
} | |
.print-only { | |
display: none !important; | |
} | |
@media print { | |
@page { | |
margin: 1.25cm 0.75cm; | |
} | |
* { | |
-webkit-box-shadow: none !important; | |
box-shadow: none !important; | |
text-shadow: none !important; | |
} | |
a { | |
color: inherit !important; | |
text-decoration: underline !important; | |
} | |
a.bare, a[href^="#"], a[href^="mailto:"] { | |
text-decoration: none !important; | |
} | |
a[href^="http:"]:not(.bare):after, a[href^="https:"]:not(.bare):after, a[href^="mailto:"]:not(.bare):after { | |
content: "(" attr(href) ")"; | |
display: inline-block; | |
font-size: 0.875em; | |
padding-left: 0.25em; | |
} | |
abbr[title]:after { | |
content: " (" attr(title) ")"; | |
} | |
pre, blockquote, tr, img { | |
page-break-inside: avoid; | |
} | |
thead { | |
display: table-header-group; | |
} | |
img { | |
max-width: 100% !important; | |
} | |
p, blockquote, dt, td.content { | |
font-size: 1em; | |
orphans: 3; | |
widows: 3; | |
} | |
h2, h3, #toctitle, .sidebarblock > .content > .title, #toctitle, .sidebarblock > .content > .title { | |
page-break-after: avoid; | |
} | |
#toc, .sidebarblock, .exampleblock > .content { | |
background: none !important; | |
} | |
#toc { | |
border-bottom: 1px solid #ddddd8 !important; | |
padding-bottom: 0 !important; | |
} | |
.sect1 { | |
padding-bottom: 0 !important; | |
} | |
.sect1 + .sect1 { | |
border: 0 !important; | |
} | |
#header > h1:first-child { | |
margin-top: 1.25rem; | |
} | |
body.book #header { | |
text-align: center; | |
} | |
body.book #header > h1:first-child { | |
border: 0 !important; | |
margin: 2.5em 0 1em 0; | |
} | |
body.book #header .details { | |
border: 0 !important; | |
display: block; | |
padding: 0 !important; | |
} | |
body.book #header .details span:first-child { | |
margin-left: 0 !important; | |
} | |
body.book #header .details br { | |
display: block; | |
} | |
body.book #header .details br + span:before { | |
content: none !important; | |
} | |
body.book #toc { | |
border: 0 !important; | |
text-align: left !important; | |
padding: 0 !important; | |
margin: 0 !important; | |
} | |
body.book #toc, body.book #preamble, body.book h1.sect0, body.book .sect1 > h2 { | |
page-break-before: always; | |
} | |
.listingblock code[data-lang]:before { | |
display: block; | |
} | |
#footer { | |
background: none !important; | |
padding: 0 0.9375em; | |
} | |
#footer-text { | |
color: rgba(0, 0, 0, 0.6) !important; | |
font-size: 0.9em; | |
} | |
.hide-on-print { | |
display: none !important; | |
} | |
.print-only { | |
display: block !important; | |
} | |
.hide-for-print { | |
display: none !important; | |
} | |
.show-for-print { | |
display: inherit !important; | |
} | |
} | |
</style> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css"> | |
</head> | |
<body class="article"> | |
<div id="header"> | |
<h1>Testing Ratpack Applications</h1> | |
<div class="details"> | |
<span id="author" class="author">Dan Hyun</span><br> | |
<span id="email" class="email">@LSpacewalker</span><br> | |
</div> | |
</div> | |
<div id="content"> | |
<div id="preamble"> | |
<div class="sectionbody"> | |
<div class="paragraph"> | |
<p>Ratpack is a developer friendly and productivity focused web framework. | |
That’s quite a claim to make. | |
We’ll explore how Ratpack’s rich testing facilities strongly support this statement.</p> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="trueintro"><a class="anchor" href="#trueintro"></a>Intro</h2> | |
<div class="sectionbody"> | |
<div class="ulist"> | |
<ul> | |
<li> | |
<p>Test framework Agnostic (Spock, JUnit, TestNG)</p> | |
</li> | |
<li> | |
<p>Core fixutres in Java 8+, first-class Groovy Support available</p> | |
</li> | |
<li> | |
<p>Most fixtures implement <code>java.lang.AutoCloseable</code></p> | |
<div class="ulist"> | |
<ul> | |
<li> | |
<p>Need to either close yourself or use in <code>try-with-resources</code></p> | |
</li> | |
<li> | |
<p>Provides points of interaction that utilize an execute around pattern in cases where you need the fixture once.</p> | |
</li> | |
</ul> | |
</div> | |
</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="truehello-world"><a class="anchor" href="#truehello-world"></a>Hello World</h2> | |
<div class="sectionbody"> | |
<div class="sect2"> | |
<h3 id="truedependencies"><a class="anchor" href="#truedependencies"></a>Dependencies</h3> | |
<div class="listingblock"> | |
<div class="title">testing-ratpack-apps.gradle</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-gradle" data-lang="gradle">plugins { <i class="conum" data-value="1"></i><b>(1)</b> | |
id 'io.ratpack.ratpack-groovy' version '1.2.0' <i class="conum" data-value="2"></i><b>(2)</b> | |
} | |
repositories { | |
jcenter() | |
} | |
dependencies { | |
runtime "org.apache.logging.log4j:log4j-slf4j-impl:${log4j}" | |
runtime "org.apache.logging.log4j:log4j-api:${log4j}" | |
runtime "org.apache.logging.log4j:log4j-core:${log4j}" | |
runtime 'com.lmax:disruptor:3.3.2' | |
testCompile ratpack.dependency('groovy-test') <i class="conum" data-value="3"></i><b>(3)</b> | |
testCompile ('org.spockframework:spock-core:1.0-groovy-2.4') { | |
exclude module: "groovy-all" | |
} | |
testCompile 'junit:junit:4.12' | |
testCompile 'org.testng:testng:6.9.10' | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Use Gradle’s incubating Plugins feature</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Pull in and apply Ratpack’s Gradle plugin from Gradle’s Plugin Portal</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Pull in <code>'io.ratpack:ratpack-groovy-test</code> from Bintray</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="truehello-world-under-test"><a class="anchor" href="#truehello-world-under-test"></a>Hello World Under Test</h3> | |
<div class="listingblock"> | |
<div class="title">ratpack.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack | |
ratpack { | |
handlers { | |
get { | |
render 'Hello Greach 2016!' | |
} | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="listingblock"> | |
<div class="title">MainClassApp</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic | |
import ratpack.func.Action | |
import ratpack.groovy.handling.GroovyContext | |
import ratpack.groovy.handling.GroovyHandler | |
import ratpack.handling.Chain | |
import ratpack.server.RatpackServer | |
import ratpack.server.RatpackServerSpec | |
@CompileStatic | |
class MainClassApp { | |
public static void main(String[] args) throws Exception { | |
RatpackServer.start({ RatpackServerSpec serverSpec -> serverSpec | |
.handlers({ Chain chain -> | |
chain.get({GroovyContext ctx -> | |
ctx.render 'Hello Greach 2016!' | |
} as GroovyHandler) | |
} as Action<Chain>) | |
} as Action<RatpackServerSpec>) | |
} | |
}</code></pre> | |
</div> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueverify"><a class="anchor" href="#trueverify"></a>Verify</h3> | |
<div class="listingblock"> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl localhost:5050 | |
Hello Greach 2016!</code></pre> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="true-code-assert-true-code"><a class="anchor" href="#true-code-assert-true-code"></a><code>assert true</code></h2> | |
<div class="sectionbody"> | |
<div class="sect2"> | |
<h3 id="truespock-hello-world"><a class="anchor" href="#truespock-hello-world"></a>Spock Hello World</h3> | |
<div class="listingblock"> | |
<div class="title">HelloWorldSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest | |
import ratpack.test.MainClassApplicationUnderTest | |
import spock.lang.AutoCleanup | |
import spock.lang.Shared | |
import spock.lang.Specification | |
import spock.lang.Unroll | |
class HelloWorldSpec extends Specification { | |
// tag::GroovyScriptAUT[] | |
@AutoCleanup | |
@Shared | |
GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest() | |
// end::GroovyScriptAUT[] | |
// tag::MainClassAUT[] | |
@AutoCleanup | |
@Shared | |
MainClassApplicationUnderTest mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp) | |
// end::MainClassAUT[] | |
@Unroll | |
def 'Should render \'Hello Greach 2016!\' from #type'() { | |
when: | |
def getText = aut.httpClient.getText() | |
then: | |
getText == 'Hello Greach 2016!' | |
where: | |
aut | type | |
groovyScriptApplicationunderTest | 'ratpack.groovy' | |
mainClassApplicationUnderTest | 'MainClassApp.groovy' | |
} | |
}</code></pre> | |
</div> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="truejunit-test"><a class="anchor" href="#truejunit-test"></a>Junit Test</h3> | |
<div class="listingblock"> | |
<div class="title">HelloJunitTest.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic | |
import org.junit.Assert | |
import org.junit.AfterClass | |
import org.junit.BeforeClass | |
import org.junit.Test | |
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest | |
import ratpack.test.MainClassApplicationUnderTest | |
import ratpack.test.http.TestHttpClient | |
import static org.junit.Assert.assertEquals | |
@CompileStatic | |
class HelloJunitTest { | |
static GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest | |
static MainClassApplicationUnderTest mainClassApplicationUnderTest | |
@BeforeClass | |
static void setup() { | |
groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest() | |
mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp) | |
} | |
@AfterClass | |
static void cleanup() { | |
groovyScriptApplicationunderTest.close() | |
mainClassApplicationUnderTest.close() | |
} | |
@Test | |
def void testHelloWorld() { | |
[ | |
groovyScriptApplicationunderTest, | |
mainClassApplicationUnderTest | |
].each { aut -> | |
TestHttpClient client = aut.httpClient | |
assertEquals('Hello Greach 2016!', client.getText()) | |
} | |
} | |
}</code></pre> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="trueunit-testing"><a class="anchor" href="#trueunit-testing"></a>Unit testing</h2> | |
<div class="sectionbody"> | |
<div class="sect2"> | |
<h3 id="true-code-groovyrequestfixture-code"><a class="anchor" href="#true-code-groovyrequestfixture-code"></a><code>GroovyRequestFixture</code></h3> | |
<div class="sect3"> | |
<h4 id="truetesting-standalone-handlers"><a class="anchor" href="#truetesting-standalone-handlers"></a>Testing Standalone Handlers</h4> | |
<div class="listingblock"> | |
<div class="title">ImportantHandler.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic | |
import ratpack.groovy.handling.GroovyContext | |
import ratpack.groovy.handling.GroovyHandler | |
@CompileStatic | |
class ImportantHandler extends GroovyHandler { | |
@Override | |
protected void handle(GroovyContext context) { | |
context.render 'Very important handler' | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ratpack.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack | |
ratpack { | |
handlers { | |
get(new ImportantHandler()) <i class="conum" data-value="1"></i><b>(1)</b> | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Bind our <code>ImportantHandler</code> to <code>GET /</code></td> | |
</tr> | |
</table> | |
</div> | |
<div class="listingblock"> | |
<div class="title">Failing test</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">def 'should render \'Very important handler\''() { | |
when: | |
HandlingResult result = GroovyRequestFixture.handle(new ImportantHandler()) {} | |
then: | |
result.bodyText == 'Very important handler <i class="conum" data-value="1"></i><b>(1)</b> | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Consult the <code>HandlingResult</code> for response body</td> | |
</tr> | |
</table> | |
</div> | |
<div class="paragraph"> | |
<p>WARN: This test will fail</p> | |
</div> | |
<div class="paragraph"> | |
<p>What happened?</p> | |
</div> | |
<div class="paragraph"> | |
<p><code>Context#render(Object)</code> uses Ratpack’s rendering framework. | |
<code>GroovyRequestFixture</code> does not actually serialize rendered objects to <code>Response</code> of <code>HandlingResult</code>. | |
For this test to pass you must either modify the Handler or modify the expectation:</p> | |
</div> | |
<div class="paragraph"> | |
<p>Modify the handler:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ImportantSendingHandler.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic | |
import ratpack.groovy.handling.GroovyContext | |
import ratpack.groovy.handling.GroovyHandler | |
@CompileStatic | |
class ImportantSendingHandler extends GroovyHandler { | |
@Override | |
protected void handle(GroovyContext context) { | |
context.response.send('Very important handler') | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="paragraph"> | |
<p>Modify the expectation:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ImportantHandlerUnitSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should render \'Very important handler\''() { | |
when: | |
HandlingResult result = GroovyRequestFixture.handle(new ImportantHandler()) {} | |
then: | |
String rendered = result.rendered(String) <i class="conum" data-value="1"></i><b>(1)</b> | |
rendered == 'Very important handler' | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Retrieve the rendered object by type from <code>HandlingResult</code></td> | |
</tr> | |
</table> | |
</div> | |
<div class="paragraph"> | |
<p>Everday use:</p> | |
</div> | |
</div> | |
<div class="sect3"> | |
<h4 id="truemodify-request-attributes"><a class="anchor" href="#truemodify-request-attributes"></a>Modify request attributes</h4> | |
<div class="listingblock"> | |
<div class="title">HeaderExtractionHandler.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.CompileStatic | |
import ratpack.groovy.handling.GroovyContext | |
import ratpack.groovy.handling.GroovyHandler | |
@CompileStatic | |
class HeaderExtractionHandler extends GroovyHandler { | |
@Override | |
protected void handle(GroovyContext context) { | |
String specialHeader = context.request.headers.get('special') ?: 'not special' <i class="conum" data-value="1"></i><b>(1)</b> | |
context.render "Special header: $specialHeader" | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Extract HTTP header and render a response to client</td> | |
</tr> | |
</table> | |
</div> | |
<div class="listingblock"> | |
<div class="title">HeaderExtractionHandlingSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.test.handling.HandlingResult | |
import spock.lang.Specification | |
import ratpack.groovy.test.handling.GroovyRequestFixture | |
import spock.lang.Unroll | |
class HeaderExtractionHandlingSpec extends Specification { | |
@Unroll | |
def 'should render #expectedValue with special header value'() { | |
when: | |
HandlingResult result = GroovyRequestFixture | |
.handle(new HeaderExtractionHandler(), requestFixture) | |
then: | |
def rendered = result.rendered(CharSequence) | |
rendered == "Special header: $expectedValue" | |
where: | |
expectedValue | requestFixture | |
'greach2016' | { header('special', 'greach2016') } <i class="conum" data-value="1"></i><b>(1)</b> | |
'not special' | {} | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>You can get a chance to configure the properties of the request to be made, can configure HTTP method, headers, request body, etc.</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect3"> | |
<h4 id="truemodify-and-make-assertions-against-context-registry"><a class="anchor" href="#truemodify-and-make-assertions-against-context-registry"></a>Modify and make assertions against context registry:</h4> | |
<div class="listingblock"> | |
<div class="title">ProfileLoadingHandler.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.Canonical | |
import groovy.transform.CompileStatic | |
import ratpack.groovy.handling.GroovyContext | |
import ratpack.groovy.handling.GroovyHandler | |
import ratpack.registry.Registry | |
@CompileStatic | |
class ProfileLoadingHandler extends GroovyHandler { | |
@Override | |
protected void handle(GroovyContext context) { | |
String role = context.request.headers.get('role') ?: 'guest' <i class="conum" data-value="1"></i><b>(1)</b> | |
String secretToken = context.get(String) <i class="conum" data-value="2"></i><b>(2)</b> | |
context.next(Registry.single(new Profile(role: role, token: secretToken))) <i class="conum" data-value="3"></i><b>(3)</b> | |
} | |
} | |
@Canonical | |
class Profile { | |
String role | |
String token | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Extract role from request header, defaulting to 'guest'</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Extract a String from the context registry</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Delegate to the next Handler in the chain and pass a new single Registry with a newly constructed Profile object</td> | |
</tr> | |
</table> | |
</div> | |
<div class="paragraph"> | |
<p>We can make use of <code>RequestFixture</code> to populate the Registry with any entries our stand-alone Handler may be expecting, such as a token in the form of a String.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ProfileLoadingHandlingSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'handler should populate context registry with Profile'() { | |
when: | |
HandlingResult result = GroovyRequestFixture.handle(new ProfileLoadingHandler()) { | |
header('role', 'admin') <i class="conum" data-value="1"></i><b>(1)</b> | |
registry { add(String, 'secret-token') } <i class="conum" data-value="2"></i><b>(2)</b> | |
} | |
then: | |
result.registry.get(Profile) == new Profile('admin', 'secret-token') <i class="conum" data-value="3"></i><b>(3)</b> | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Use <code>RequestFixture#header</code> to add Headers to the HTTP Request</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Use <code>RequestFixture#registry</code> to add a <code>String</code> to the Context registry</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Consult the HandlingResponse to ensure that the context was populated with a <code>Profile</code> object and that it meets our expectations</td> | |
</tr> | |
</table> | |
</div> | |
<div class="paragraph"> | |
<p>Let’s put our <code>ProfileLoadingHandler</code> in a chain with a dummy Map renderer:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ProfileLoadingHandlingSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should be able to render Profile as map from Registry'() { | |
when: | |
HandlingResult result = GroovyRequestFixture.handle(new GroovyChainAction() { <i class="conum" data-value="1"></i><b>(1)</b> | |
@Override | |
void execute() throws Exception { | |
all(new ProfileLoadingHandler()) <i class="conum" data-value="2"></i><b>(2)</b> | |
get { <i class="conum" data-value="3"></i><b>(3)</b> | |
Profile profile = get(Profile) | |
render([profile: [role: profile.role, token: profile.token]]) | |
} | |
} | |
}) { | |
header('role', 'admin') | |
registry { add(String, 'secret-token') } | |
} | |
then: | |
result.rendered(Map) == [profile: [role: 'admin', 'token': 'secret-token']] <i class="conum" data-value="4"></i><b>(4)</b> | |
}</code></pre> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="true-code-groovyembeddedapp-code"><a class="anchor" href="#true-code-groovyembeddedapp-code"></a><code>GroovyEmbeddedApp</code></h2> | |
<div class="sectionbody"> | |
<div class="paragraph"> | |
<p><code>GroovyEmbeddedApp</code> represents an isolated subset of functionality that stands up a full Ratpack server.</p> | |
</div> | |
<div class="paragraph"> | |
<p>It represents a very bare server that binds to an ephemeral port and has no base directory by default. | |
<code>GroovyEmbeddedApp</code> is also <code>AutoCloseable</code>. | |
If you plan on making more than a few interactions it may help to grab a <code>TestHttpClient</code> from the server, otherwise you can make use of <code>EmbeddedApp#test(TestHttpClient)</code> which will ensure that the <code>EmbeddedApp</code> is shut down gracefully. | |
Javadocs for Ratpack are 100% tested and make use of <code>EmbeddedApp</code> to demonstrate functionality.</p> | |
</div> | |
<div class="paragraph"> | |
<p>The <code>EmbeddedApp</code> is also useful in creating a test fixture that represents some network based resource that returns canned or contrived responses.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">EmbeddedAppSpec</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'can create simple hello world'() { | |
expect: | |
GroovyEmbeddedApp.fromHandler { <i class="conum" data-value="1"></i><b>(1)</b> | |
render 'Hello Greach 2016!' | |
} test { | |
assert getText() == 'Hello Greach 2016!' <i class="conum" data-value="2"></i><b>(2)</b> | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Creates a full Ratpack server with a single handler</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Ratpack provides us with a <code>TestHttpClient</code> that is configured to submit requests to <code>EmbeddedApp</code>. When the closure is finished executing Ratpack will take care of cleaning up the <code>EmbeddedApp</code>.</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="truetesthttpclient"><a class="anchor" href="#truetesthttpclient"></a>TestHttpClient</h2> | |
<div class="sectionbody"> | |
<div class="paragraph"> | |
<p>For testing, Ratpack provides <code>TestHttpClient</code> which is a blocking, synchronous http client for making requests against a running <code>ApplicationUnderTest</code>. This is intentionally designed in order to make testing deterministic and predictable.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">EmbeddedAppSpec</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'demonstrate ByMethodSpec'() { | |
given: | |
GroovyEmbeddedApp app = GroovyEmbeddedApp.fromHandlers { <i class="conum" data-value="1"></i><b>(1)</b> | |
path { | |
byMethod { | |
get { | |
render 'GET' | |
} | |
post { | |
render 'POST' | |
} | |
} | |
} | |
} | |
and: | |
TestHttpClient client = app.httpClient <i class="conum" data-value="2"></i><b>(2)</b> | |
expect: <i class="conum" data-value="3"></i><b>(3)</b> | |
client.getText() == 'GET' | |
client.postText() == 'POST' | |
client.put().status.code == 405 | |
client.delete().status.code == 405 | |
cleanup: <i class="conum" data-value="4"></i><b>(4)</b> | |
app.close() | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Create <code>GroovyEmbeddedApp</code> from a chain</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Retrieve a configured <code>TestHttpClient</code> for making requests against the <code>EmbeddedApp</code></td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Make some assertions about the application as described by the chain</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="4"></i><b>4</b></td> | |
<td>Have Spock invoke <code>EmbeddedApp#close</code> to gracefully shutdown the server.</td> | |
</tr> | |
</table> | |
</div> | |
<div class="paragraph"> | |
<p>The <code>TestHttpClient</code> has some basic support for manipulating request configuration as well as handling redirects and cookies.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">EmbeddedAppSpec</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> def 'should handle redirects and cookies'() { | |
expect: | |
GroovyEmbeddedApp.fromHandlers { <i class="conum" data-value="1"></i><b>(1)</b> | |
get { | |
render request.oneCookie('foo') ?: 'empty' | |
} | |
get('set') { | |
response.cookie('foo', 'foo') | |
redirect '/' | |
} | |
get('clear') { | |
response.expireCookie('foo') | |
redirect '/' | |
} | |
} test { | |
assert getText() == 'empty' <i class="conum" data-value="2"></i><b>(2)</b> | |
assert getCookies('/')*.name() == [] | |
assert getCookies('/')*.value() == [] | |
assert getText('set') == 'foo' | |
assert getCookies('/')*.name() == ['foo'] | |
assert getCookies('/')*.value() == ['foo'] | |
assert getText() == 'foo' | |
assert getText('clear') == 'empty' | |
assert getCookies('/')*.name() == [] | |
assert getCookies('/')*.value() == [] | |
assert getText() == 'empty' | |
assert getCookies('/')*.name() == [] | |
assert getCookies('/')*.value() == [] | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Create sample app that reads and writes cookies</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Issue requests that ensures cookie setting/expiring and redirect functionality</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="trueasync-testing"><a class="anchor" href="#trueasync-testing"></a>Async Testing</h2> | |
<div class="sectionbody"> | |
<div class="paragraph"> | |
<p>Ratpack is asynchronous and non-blocking from the ground up. This means that not only is Ratpack’s api asynchronous but it expects that your code should be asynchronous as well.</p> | |
</div> | |
<div class="paragraph"> | |
<p>Let’s say we have a <code>ProfileService</code> that’s responsible for retrieving `Profile`s:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ProfileService.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import groovy.transform.Canonical | |
import ratpack.exec.Operation | |
import ratpack.exec.Promise | |
class ProfileService { | |
final List<Profile> profiles = [] | |
Promise<List<Profile>> getProfiles() { | |
Promise.value(profiles) | |
} | |
Operation add(Profile p) { | |
profiles.add(p) | |
Operation.noop() | |
} | |
Operation delete() { | |
profiles.clear() | |
Operation.noop() | |
} | |
} | |
@Canonical | |
class Profile { | |
String role | |
String token | |
}</code></pre> | |
</div> | |
</div> | |
<div class="paragraph"> | |
<p>If you were to test this Service without any assistance from Ratpack you will run into the well known <code>UnmanagedThreadException</code>:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="content"> | |
<pre>ratpack.exec.UnmanagedThreadException: Operation attempted on non Ratpack managed thread</pre> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueexecharness"><a class="anchor" href="#trueexecharness"></a>ExecHarness</h3> | |
<div class="paragraph"> | |
<p><code>ExecHarness</code> is the utility that Ratpack provides to test any kind of asynchronous behavior. | |
Unsurprisingly <code>ExecHarness</code> is also an <code>AutoCloseable</code>. | |
It utilizes resources that manage an <code>EventLoopGroup</code> and an <code>ExecutorService</code> so it’s important to make sure these resources get properly cleaned up.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ProfileServiceExec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.exec.ExecResult | |
import ratpack.exec.Promise | |
import ratpack.test.exec.ExecHarness | |
import spock.lang.AutoCleanup | |
import spock.lang.Specification | |
class ProfileServiceSpec extends Specification { | |
@AutoCleanup | |
ExecHarness execHarness = ExecHarness.harness() <i class="conum" data-value="1"></i><b>(1)</b> | |
def 'can add/retrieve/remove profiles from service'() { | |
given: | |
ProfileService service = new ProfileService() | |
when: | |
ExecResult<Promise<List<Profile>>> result = execHarness.yield { service.profiles } <i class="conum" data-value="2"></i><b>(2)</b> | |
then: | |
result.value == [] | |
when: | |
execHarness.yield { service.add(new Profile(role: 'admin', token: 'secret')) } | |
and: | |
List<Profile> profiles = execHarness.yield { service.profiles }.value | |
then: profiles == [new Profile(role: 'admin', token: 'secret')] | |
when: | |
execHarness.yield { service.delete() } | |
then: | |
execHarness.yield { service.profiles }.value == [] | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Create an <code>ExecHarness</code> and tell Spock to clean up when we are finished</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Use <code>ExecHarness#yield</code> to wrap all of our service calls so that our Promises and Operations can be resolved on a Ratpack managed thread.</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="truefunctional-testing"><a class="anchor" href="#truefunctional-testing"></a>Functional testing</h2> | |
<div class="sectionbody"> | |
<div class="sect2"> | |
<h3 id="truemainclassapplicationundertest"><a class="anchor" href="#truemainclassapplicationundertest"></a>MainClassApplicationUnderTest</h3> | |
<div class="dlist"> | |
<dl> | |
<dt class="hdlist1"><code>GroovyRatpackMainApplicationUnderTest</code></dt> | |
<dd> | |
<p>For testing <code>ratpack.groovy</code> backed applications</p> | |
</dd> | |
</dl> | |
</div> | |
<div class="listingblock"> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> @AutoCleanup | |
@Shared | |
GroovyRatpackMainApplicationUnderTest groovyScriptApplicationunderTest = new GroovyRatpackMainApplicationUnderTest()</code></pre> | |
</div> | |
</div> | |
<div class="dlist"> | |
<dl> | |
<dt class="hdlist1"><code>MainClassApplicationUnderTest</code></dt> | |
<dd> | |
<p>For testing class backed applications</p> | |
</dd> | |
</dl> | |
</div> | |
<div class="listingblock"> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> @AutoCleanup | |
@Shared | |
MainClassApplicationUnderTest mainClassApplicationUnderTest = new MainClassApplicationUnderTest(MainClassApp)</code></pre> | |
</div> | |
</div> | |
<div class="paragraph"> | |
<p>Our sample Ratpack application for testing:</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ratpack.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import static ratpack.groovy.Groovy.ratpack | |
ratpack { | |
serverConfig { | |
sysProps() <i class="conum" data-value="1"></i><b>(1)</b> | |
require('/api', ApiConfig) <i class="conum" data-value="2"></i><b>(2)</b> | |
} | |
bindings { | |
bind(ConfService) <i class="conum" data-value="3"></i><b>(3)</b> | |
} | |
handlers { | |
get { ConfService confService -> | |
confService.conferences.map { <i class="conum" data-value="4"></i><b>(4)</b> | |
"Here are the best conferences: $it" | |
} then(context.&render) | |
} | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Pull configuration from System properties</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Create an ApiConfig object and put into the registry</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Bind <code>ConfService</code> using Guice</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="4"></i><b>4</b></td> | |
<td>Use <code>ConfService</code> to retrieve list of awesome Groovy Conferences</td> | |
</tr> | |
</table> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ApiConfig.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">class ApiConfig { | |
String url | |
}</code></pre> | |
</div> | |
</div> | |
<div class="paragraph"> | |
<p>Simple object to contain our configuration data related to an API</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ConfService</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import com.google.inject.Inject | |
import ratpack.exec.Promise | |
import ratpack.http.client.HttpClient | |
class ConfService { | |
final HttpClient httpClient | |
final ApiConfig apiConfig | |
@Inject | |
ConfService(ApiConfig apiConfig, HttpClient httpClient) { <i class="conum" data-value="1"></i><b>(1)</b> | |
this.apiConfig = apiConfig | |
this.httpClient = httpClient | |
} | |
Promise<List<String>> getConferences() { <i class="conum" data-value="2"></i><b>(2)</b> | |
httpClient.get(new URI(apiConfig.url)) | |
.map { it.body } | |
.map { it.text.split(',').collect { it } } | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Receive <code>ApiConfig</code> and <code>HtpClient</code> from Guice</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Define an asynchronous service method to retrieve data from remote service</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueconfiguration"><a class="anchor" href="#trueconfiguration"></a>Configuration</h3> | |
<div class="paragraph"> | |
<p>We can take advantage of system properties to change how the Ratpack application configures its services.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">FunctionalSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest | |
import ratpack.groovy.test.embed.GroovyEmbeddedApp | |
import ratpack.test.ApplicationUnderTest | |
import ratpack.test.http.TestHttpClient | |
import spock.lang.AutoCleanup | |
import spock.lang.Shared | |
import spock.lang.Specification | |
class FunctionalSpec extends Specification { | |
@Shared | |
@AutoCleanup | |
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() <i class="conum" data-value="1"></i><b>(1)</b> | |
@Delegate | |
TestHttpClient client = aut.httpClient <i class="conum" data-value="2"></i><b>(2)</b> | |
@Shared | |
@AutoCleanup | |
GroovyEmbeddedApp api = GroovyEmbeddedApp.fromHandler { <i class="conum" data-value="3"></i><b>(3)</b> | |
render 'Greach, GR8conf, Gradle Conf' | |
} | |
def setup() { | |
System.setProperty('ratpack.api.url', api.address.toURL().toString()) <i class="conum" data-value="4"></i><b>(4)</b> | |
} | |
def 'can get best conferences'() { <i class="conum" data-value="5"></i><b>(5)</b> | |
when: | |
get() | |
then: | |
response.statusCode == 200 | |
and: | |
response.body.text.contains('Greach') | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Create our <code>ApplicationUnderTest</code> and tell Spock to clean up when we’re done</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>Retrieve <code>TestHttpClient</code> and make use of <code>@Delegate</code> to make tests very readable</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>Create a simple service that response with a comma separated list of Groovy Conferences</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="4"></i><b>4</b></td> | |
<td>Set system property to point to our stubbed service</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="5"></i><b>5</b></td> | |
<td>Write a simple test to assure that our Ratpack app can make a succcessful call to the remote api</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueimpositions"><a class="anchor" href="#trueimpositions"></a>Impositions</h3> | |
<div class="paragraph"> | |
<p><code>Impositions</code> allow a user to provide overrides to various aspects of the Ratpack application bootstrap phase.</p> | |
</div> | |
<div class="ulist"> | |
<ul> | |
<li> | |
<p><code>ServerConfigImposition</code> allows to override server configuration</p> | |
</li> | |
<li> | |
<p><code>BindingsImposition</code> allows to provide Guice binding overrides</p> | |
</li> | |
<li> | |
<p><code>UserRegistryImposition</code> allows you to provide alternatives for items in the registry</p> | |
</li> | |
</ul> | |
</div> | |
<div class="listingblock"> | |
<div class="title">ImpositionSpec</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.exec.Promise | |
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest | |
import ratpack.impose.UserRegistryImposition | |
import ratpack.impose.ImpositionsSpec | |
import ratpack.test.ApplicationUnderTest | |
import ratpack.test.http.TestHttpClient | |
import spock.lang.AutoCleanup | |
import spock.lang.Shared | |
import spock.lang.Specification | |
import ratpack.guice.Guice | |
class ImpositionSpec extends Specification { | |
@Shared | |
@AutoCleanup | |
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() { | |
@Override | |
protected void addImpositions(ImpositionsSpec impositions) { <i class="conum" data-value="1"></i><b>(1)</b> | |
impositions.add( | |
UserRegistryImposition.of(Guice.registry { | |
it.add(new ConfService(null, null) { | |
Promise<List<String>> getConferences() { | |
Promise.value(['Greach']) | |
} | |
}) | |
})) | |
} | |
} | |
@Delegate | |
TestHttpClient client = aut.httpClient | |
def 'can get list of gr8 conferences'() { | |
when: | |
get() | |
then: | |
response.statusCode == 200 | |
and: | |
response.body.text.contains('Greach') | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>Override <code>addImpositions</code> method to provide a <code>UserRegistryImposition</code> that supplies our own dumb implementation of <code>ConfService</code> that does not need to make any network connections</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueremotecontrol"><a class="anchor" href="#trueremotecontrol"></a>RemoteControl</h3> | |
<div class="paragraph"> | |
<p>Authored by Luke Daley; originally for Grails</p> | |
</div> | |
<div class="paragraph"> | |
<p>Used to serialize commands to be executed on the <code>ApplicationUnderTest</code></p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">build.gradle</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy"> testCompile ratpack.dependency('remote-test')</code></pre> | |
</div> | |
</div> | |
<div class="paragraph"> | |
<p>Here we add a test compile dependency on <code>io.ratpack:ratpack-remote-test</code> which includes a dependency on <code>remote-control</code></p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">RemoteControlSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import io.remotecontrol.client.UnserializableResultStrategy | |
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest | |
import ratpack.guice.BindingsImposition | |
import ratpack.impose.ImpositionsSpec | |
import ratpack.remote.RemoteControl | |
import ratpack.test.ApplicationUnderTest | |
import ratpack.test.http.TestHttpClient | |
import spock.lang.AutoCleanup | |
import spock.lang.Shared | |
import spock.lang.Specification | |
class RemoteControlSpec extends Specification { | |
@Shared | |
@AutoCleanup | |
ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest() { | |
@Override | |
protected void addImpositions(ImpositionsSpec impositions) { | |
impositions.add(BindingsImposition.of { | |
it.bindInstance RemoteControl.handlerDecorator() <i class="conum" data-value="1"></i><b>(1)</b> | |
}) | |
} | |
} | |
@Delegate | |
TestHttpClient client = aut.httpClient | |
ratpack.test.remote.RemoteControl remoteControl = new ratpack.test.remote.RemoteControl(aut, UnserializableResultStrategy.NULL) <i class="conum" data-value="2"></i><b>(2)</b> | |
def 'should render profiles'() { | |
when: | |
get() | |
then: | |
response.body.text == '[]' | |
when: | |
remoteControl.exec { <i class="conum" data-value="3"></i><b>(3)</b> | |
get(ProfileService) | |
.add(new Profile('admin')) | |
} | |
and: | |
get() | |
then: | |
response.body.text.startsWith('[{') | |
} | |
}</code></pre> | |
</div> | |
</div> | |
<div class="colist arabic"> | |
<table> | |
<tr> | |
<td><i class="conum" data-value="1"></i><b>1</b></td> | |
<td>We use <code>BindingsImposition</code> here to add a hook into the running <code>ApplicationUnderTest</code> that allows us to run remote code on the server</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="2"></i><b>2</b></td> | |
<td>We tell <code>RemoteControl</code> not to complain if the result of the command is not Serializable</td> | |
</tr> | |
<tr> | |
<td><i class="conum" data-value="3"></i><b>3</b></td> | |
<td>We use remote control here to grab the <code>ProfileService</code> and manually add a profile</td> | |
</tr> | |
</table> | |
</div> | |
</div> | |
<div class="sect2"> | |
<h3 id="trueephemeralbasedir"><a class="anchor" href="#trueephemeralbasedir"></a>EphemeralBaseDir</h3> | |
<div class="paragraph"> | |
<p>A utility that provides a nice way to interact with files that would provide the basis of a base directory for Ratpack applications. | |
It is also an <code>AutoCloseable</code> so you’ll need to make sure to clean up after use.</p> | |
</div> | |
<div class="listingblock"> | |
<div class="title">EphemeralSpec.groovy</div> | |
<div class="content"> | |
<pre class="highlightjs highlight"><code class="language-groovy" data-lang="groovy">import ratpack.groovy.test.embed.GroovyEmbeddedApp | |
import ratpack.test.embed.EphemeralBaseDir | |
import spock.lang.Specification | |
import java.nio.file.Path | |
class EphemeralSpec extends Specification { | |
def 'can supply ephemeral basedir'() { | |
expect: | |
EphemeralBaseDir.tmpDir().use { baseDir -> | |
baseDir.write("mydir/.ratpack", "") | |
baseDir.write("mydir/assets/message.txt", "Hello Ratpack!") | |
Path mydir = baseDir.getRoot().resolve("mydir") | |
ClassLoader classLoader = new URLClassLoader((URL[]) [mydir.toUri().toURL()].toArray()) | |
Thread.currentThread().setContextClassLoader(classLoader); | |
GroovyEmbeddedApp.of { serverSpec -> | |
serverSpec | |
.serverConfig { c -> c.baseDir(mydir) } | |
.handlers { chain -> | |
chain.files { f -> f.dir("assets") } | |
} | |
}.test { | |
String message = getText("message.txt") | |
assert "Hello Ratpack!" == message | |
} | |
} | |
} | |
}</code></pre> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="sect1"> | |
<h2 id="trueresources"><a class="anchor" href="#trueresources"></a>Resources</h2> | |
<div class="sectionbody"> | |
<div class="ulist"> | |
<ul> | |
<li> | |
<p><a href="http://shop.oreilly.com/product/0636920037545.do">(book) O’Reilly: Learning Ratpack by Dan Woods</a></p> | |
</li> | |
<li> | |
<p><a href="https://ratpack.io/manual/current/api/ratpack/test/package-summary.html">(javadocs) Ratpack Test</a></p> | |
</li> | |
<li> | |
<p><a href="https://ratpack.io/manual/current/api/ratpack/groovy/test/package-summary.html">(javadocs) Ratpack Groovy Test</a></p> | |
</li> | |
<li> | |
<p><a href="https://ratpack.io/manual/current/api/ratpack/remote/package-summary.html">(javadocs) Ratpack Remote</a></p> | |
</li> | |
<li> | |
<p><a href="https://ratpack.io/manual/current/api/ratpack/test/remote/package-summary.html">(javadocs) Ratpack Remote Test</a></p> | |
</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="footer"> | |
<div id="footer-text"> | |
Last updated 2016-04-08 22:56:00 EDT | |
</div> | |
</div> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/styles/github.min.css"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/highlight.min.js"></script> | |
<script>hljs.initHighlighting()</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment