├── .editorconfig ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── assets └── fonts │ └── leaguegothic │ ├── LICENSE │ └── league_gothic-webfont.ttf ├── css ├── main.css ├── print.css ├── reset.css └── webfonts │ ├── BryantWebMedium.eot │ ├── BryantWebMedium.ttf │ └── BryantWebMedium.woff ├── examples ├── casper │ ├── drupal.js │ ├── package-lock.json │ ├── package.json │ ├── picturefill.js │ ├── test-js-code.js │ └── user-actions.js ├── grunt │ ├── devperf │ │ ├── Gruntfile.js │ │ └── package.json │ ├── jshint │ │ ├── Gruntfile.js │ │ ├── example.js │ │ └── package.json │ ├── pagespeed │ │ ├── Gruntfile.js │ │ └── package.json │ ├── perfbudget │ │ ├── Gruntfile.js │ │ └── package.json │ └── phantomas │ │ ├── Gruntfile.js │ │ └── package.json ├── targets │ ├── README.md │ ├── jquery-3.3.1-min.js │ ├── modernizr-3.6.0-custom.js │ ├── test-js-code.html │ ├── test-user-actions-p1.html │ ├── test-user-actions-p2.html │ └── test-user-actions-p3.html └── wraith │ ├── configs │ └── demo.yaml │ └── javascript │ └── snap.js ├── favicon.ico ├── img ├── 4k-logo-square-500px.png ├── browserstack-screenshots.png ├── delaypackage.gif ├── grunt-devperf-graph.png ├── grunt-perfbudget-output.png ├── grunt-phantomas-graph.png ├── jshint.png ├── timing-overview.png ├── visitor-flow.png └── wraith-example.png ├── index.html ├── js ├── reveal.js └── reveal.min.js └── lib ├── classList.js ├── github.css ├── prism.css ├── prism.js └── zenburn.css /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig for my slides 2 | [*.*] 3 | indent_style = space 4 | indent_size = 2 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all node.js dependencies 2 | # - if you want to use the grunt.js examples, run "npm install" in each directory 3 | *node_modules 4 | 5 | # ignore Wraith 6 | examples/wraith/wraith/* 7 | 8 | # ignore grunt-pagespeed API key file 9 | examples/grunt/pagespeed/settings.json 10 | 11 | # ignore grunt-perfbudget API key file 12 | examples/grunt/perfbudget/settings.json 13 | 14 | # ignore example grunt-phantomas output 15 | examples/grunt/phantomas/reports/* 16 | examples/grunt/phantomas/screenshots/* 17 | 18 | # ignore example grunt-devperf output 19 | examples/grunt/devperf/reports/* 20 | 21 | # ignore wraith output 22 | examples/wraith/shots -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.1 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | For the presentation: http://creativecommons.org/licenses/by/3.0/ 2 | 3 | Author: Chris Ruppel 4 | 5 | -------------------------------------------------------------------------------- 6 | 7 | For [reveal.js](http://lab.hakim.se/reveal-js/): 8 | 9 | Copyright (C) 2012 Hakim El Hattab, http://hakim.se 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automated Frontend Testing 2 | 3 | An intro to automated frontend testing. This is an enormous field containing 4 | many skillsets, but this presentation covers the basics of testing in the 5 | context of building websites. It covers functional testing, performance testing, 6 | and QA tools for development teams. 7 | 8 | ## Examples 9 | 10 | This slide deck comes with many examples of the concepts discussed within the 11 | slides. Browse the `examples` folder to experiment with real, working code 12 | snippets to help get you started. 13 | 14 | ## Presented at: 15 | 16 | * [DrupalCon Austin 2014](https://austin2014.drupal.org/session/automated-frontend-testing.html) 17 | * [DrupalCon Amsterdam 2014](https://amsterdam2014.drupal.org/session/automated-frontend-testing.html) 18 | * [Frontend United 2015](https://frontendunited.org/) 19 | -------------------------------------------------------------------------------- /assets/fonts/leaguegothic/LICENSE: -------------------------------------------------------------------------------- 1 | SIL Open Font License (OFL) 2 | http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL 3 | -------------------------------------------------------------------------------- /assets/fonts/leaguegothic/league_gothic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/assets/fonts/leaguegothic/league_gothic-webfont.ttf -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Main styles for reveal.js 3 | * 4 | * @author Hakim El Hattab 5 | */ 6 | 7 | 8 | /********************************************* 9 | * FONT-FACE DEFINITIONS 10 | *********************************************/ 11 | 12 | @font-face { 13 | font-family: 'Bryant'; 14 | src: url('webfonts/BryantWebMedium.eot'); /* IE9 Compat Modes */ 15 | src: url('webfonts/BryantWebMedium.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 16 | url('webfonts/BryantWebMedium.woff') format('woff'), /* Modern Browsers */ 17 | url('webfonts/BryantWebMedium.ttf') format('truetype'); /* Safari, Android, iOS */ 18 | } 19 | 20 | 21 | /********************************************* 22 | * GLOBAL STYLES 23 | *********************************************/ 24 | 25 | html, body { 26 | padding: 0; 27 | margin: 0; 28 | width: 100%; 29 | height: 100%; 30 | min-height: 600px; 31 | } 32 | 33 | body { 34 | position: relative; 35 | padding: 0; 36 | margin: 0; 37 | overflow: hidden; 38 | 39 | font-family: 'Bryant', 'Helvetica Neue', Helvetica, sans-serif; 40 | font-size: 36px; 41 | font-weight: 200; 42 | letter-spacing: -0.02em; 43 | color: #4D4D4D; 44 | 45 | background: #1c1e20; 46 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPHJhZGlhbEdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IjUwJSIgcj0iNzUlIj4KICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1NTVhNWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogICAgPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMWMxZTIwIiBzdG9wLW9wYWNpdHk9IjEiLz4KICA8L3JhZGlhbEdyYWRpZW50PgogIDxyZWN0IHg9Ii01MCIgeT0iLTUwIiB3aWR0aD0iMTAxIiBoZWlnaHQ9IjEwMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 47 | background: -moz-radial-gradient(center, ellipse cover, rgba(85,90,95,1) 0%, rgba(28,30,32,1) 100%); 48 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(85,90,95,1)), color-stop(100%,rgba(28,30,32,1))); 49 | background: -webkit-radial-gradient(center, ellipse cover, rgba(85,90,95,1) 0%,rgba(28,30,32,1) 100%); 50 | background: -o-radial-gradient(center, ellipse cover, rgba(85,90,95,1) 0%,rgba(28,30,32,1) 100%); 51 | background: -ms-radial-gradient(center, ellipse cover, rgba(85,90,95,1) 0%,rgba(28,30,32,1) 100%); 52 | background: radial-gradient(center, ellipse cover, rgba(85,90,95,1) 0%,rgba(28,30,32,1) 100%); 53 | } 54 | 55 | /********************************************* 56 | * HEADERS 57 | *********************************************/ 58 | .reveal h1, 59 | .reveal h2, 60 | .reveal h3, 61 | .reveal h4 { 62 | margin: 0 0 20px 0; 63 | 64 | color: #4D4D4D; 65 | 66 | font-family: 'Bryant', 'Helvetica Neue', Helvetica, sans-serif; 67 | line-height: 0.9em; 68 | letter-spacing: 0.02em; 69 | 70 | text-shadow: 0px 0px 6px rgba(0,0,0,0.2); 71 | } 72 | 73 | .reveal .up { 74 | text-transform: uppercase; 75 | } 76 | 77 | .reveal h1 { font-size: 136px; } 78 | .reveal h2 { font-size: 76px; } 79 | .reveal h3 { font-size: 56px; } 80 | .reveal h4 { font-size: 36px; } 81 | 82 | .reveal h1.inverted, 83 | .reveal h2.inverted, 84 | .reveal h3.inverted, 85 | .reveal h4.inverted { 86 | color: #4D4D4D; 87 | text-shadow: 0px 0px 2px rgba(0,0,0,0.2); 88 | } 89 | 90 | .reveal h1 { 91 | text-shadow: 0 1px 0 #ccc, 92 | 0 2px 0 #c9c9c9, 93 | 0 3px 0 #bbb, 94 | 0 4px 0 #b9b9b9, 95 | 0 5px 0 #aaa, 96 | 0 6px 1px rgba(0,0,0,.1), 97 | 0 0 5px rgba(0,0,0,.1), 98 | 0 1px 3px rgba(0,0,0,.3), 99 | 0 3px 5px rgba(0,0,0,.2), 100 | 0 5px 10px rgba(0,0,0,.25), 101 | 0 20px 20px rgba(0,0,0,.15); 102 | } 103 | 104 | 105 | /********************************************* 106 | * VIEW FRAGMENTS 107 | *********************************************/ 108 | 109 | .reveal .slides section .fragment { 110 | opacity: 0; 111 | 112 | -webkit-transition: all .2s ease; 113 | -moz-transition: all .2s ease; 114 | -ms-transition: all .2s ease; 115 | -o-transition: all .2s ease; 116 | transition: all .2s ease; 117 | } 118 | .reveal .slides section .fragment.visible { 119 | opacity: 1; 120 | } 121 | 122 | 123 | /********************************************* 124 | * DEFAULT ELEMENT STYLES 125 | *********************************************/ 126 | 127 | .reveal .slides section { 128 | line-height: 1.2em; 129 | font-weight: normal; 130 | } 131 | 132 | .reveal strong, 133 | .reveal b { 134 | font-weight: bold; 135 | } 136 | 137 | .reveal em, 138 | .reveal i { 139 | font-style: italic; 140 | } 141 | 142 | .reveal ol, 143 | .reveal ul { 144 | display: inline-block; 145 | 146 | text-align: left; 147 | margin: 0 auto; 148 | } 149 | 150 | .reveal ol { 151 | list-style-type: decimal; 152 | } 153 | 154 | .reveal ul { 155 | list-style-type: disc; 156 | } 157 | 158 | .reveal ul ul { 159 | list-style-type: square; 160 | } 161 | 162 | .reveal ul ul ul { 163 | list-style-type: circle; 164 | } 165 | 166 | .reveal ul ul, 167 | .reveal ul ol, 168 | .reveal ol ol, 169 | .reveal ol ul { 170 | display: block; 171 | margin-left: 40px; 172 | } 173 | 174 | .reveal ul.checks { 175 | list-style-type: none; 176 | } 177 | .reveal ul.checks li { 178 | margin-top: .5em; 179 | } 180 | .reveal ul.checks li:before { 181 | content: '\2714 '; 182 | color: green; 183 | } 184 | .reveal ul.white li:before { 185 | color: white; 186 | } 187 | 188 | .reveal p { 189 | margin-bottom: 10px; 190 | } 191 | 192 | .reveal blockquote { 193 | display: block; 194 | position: relative; 195 | margin: 5px auto; 196 | padding: 5px; 197 | 198 | font-style: italic; 199 | background: rgba(255, 255, 255, 0.05); 200 | box-shadow: 0px 0px 2px rgba(0,0,0,0.2); 201 | } 202 | .reveal blockquote p:before { 203 | content: '“'; 204 | } 205 | .reveal blockquote p:after { 206 | content: '”'; 207 | } 208 | 209 | .reveal pre { 210 | display: block; 211 | position: relative; 212 | width: 120%; 213 | left: -10%; 214 | font-size: .8em; 215 | } 216 | .reveal code { 217 | background: #272822; 218 | font-family: monospace; 219 | } 220 | 221 | 222 | .reveal table th, 223 | .reveal table td { 224 |   text-align: left; 225 |    padding-right: .3em; 226 | } 227 | 228 | .reveal table th { 229 |   text-shadow: rgb(255,255,255) 1px 1px 2px; 230 | } 231 | 232 | .reveal small { 233 | font-size: 60%; 234 | line-height: 1em; 235 | vertical-align: top; 236 | } 237 | 238 | .reveal q { 239 | font-style: italic; 240 | } 241 | .reveal q:before { 242 | content: '“'; 243 | } 244 | .reveal q:after { 245 | content: '”'; 246 | } 247 | 248 | .reveal a:not(.image) { 249 | color: #569CCD; 250 | text-decoration: none; 251 | 252 | -webkit-transition: all .2s ease; 253 | -moz-transition: all .2s ease; 254 | -ms-transition: all .2s ease; 255 | -o-transition: all .2s ease; 256 | transition: all .2s ease; 257 | } 258 | 259 | .reveal a:not(.image):hover { 260 | color: hsl(185, 85%, 70%); 261 | background: hsla(185, 25%, 20%, 0.4); 262 | text-shadow: none; 263 | border: none; 264 | border-radius: 2px; 265 | } 266 | 267 | .reveal section img { 268 | margin: 30px 0 0 0; 269 | background: rgba(255,255,255,0.12); 270 | border: 4px solid #eee; 271 | 272 | -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); 273 | -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); 274 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); 275 | 276 | -webkit-transition: all .2s linear; 277 | -moz-transition: all .2s linear; 278 | -ms-transition: all .2s linear; 279 | -o-transition: all .2s linear; 280 | transition: all .2s linear; 281 | } 282 | 283 | .reveal a:hover img { 284 | background: rgba(255,255,255,0.2); 285 | border-color: #66B360; 286 | 287 | -webkit-box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); 288 | -moz-box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); 289 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); 290 | } 291 | 292 | /* Allow for bigger images */ 293 | .big-img .reveal section img { 294 | margin-top: -50px; 295 | margin-left: -150px; 296 | } 297 | 298 | img.bare { 299 | border: 0; 300 | background: none; 301 | box-shadow: none; 302 | } 303 | 304 | /* Remove text-transform */ 305 | .reveal .nt { 306 | text-transform: none; 307 | } 308 | 309 | /* Examples should be subdued */ 310 | .reveal .example { 311 | font-size: .9em; 312 | color: #888; 313 | } 314 | 315 | 316 | /********************************************* 317 | * CONTROLS 318 | *********************************************/ 319 | 320 | .reveal .controls { 321 | display: none; 322 | position: fixed; 323 | width: 100px; 324 | height: 100px; 325 | z-index: 30; 326 | 327 | right: 0; 328 | bottom: 0; 329 | } 330 | 331 | .reveal .controls a { 332 | font-size: 30px; 333 | position: absolute; 334 | opacity: 0.1; 335 | color: #fff; 336 | } 337 | .reveal .controls a.enabled { 338 | opacity: 0.6; 339 | color: #7cb977; 340 | 341 | text-shadow: 0px 0px 2px hsla(185, 45%, 70%, 0.3); 342 | } 343 | .reveal .controls a.enabled:active { 344 | margin-top: 1px; 345 | } 346 | 347 | .reveal .controls .left { 348 | top: 30px; 349 | } 350 | 351 | .reveal .controls .right { 352 | left: 60px; 353 | top: 30px; 354 | } 355 | 356 | .reveal .controls .up { 357 | left: 30px; 358 | } 359 | 360 | .reveal .controls .down { 361 | left: 30px; 362 | top: 60px; 363 | 364 | } 365 | 366 | 367 | /********************************************* 368 | * PROGRESS BAR 369 | *********************************************/ 370 | 371 | .reveal .progress { 372 | position: fixed; 373 | display: none; 374 | height: 3px; 375 | width: 100%; 376 | bottom: 0; 377 | left: 0; 378 | 379 | background: rgba(0,0,0,0.2); 380 | } 381 | 382 | .reveal .progress span { 383 | display: block; 384 | background: #66B360; 385 | height: 100%; 386 | width: 0px; 387 | 388 | -webkit-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 389 | -moz-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 390 | -ms-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 391 | -o-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 392 | transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 393 | } 394 | 395 | /********************************************* 396 | * ROLLING LINKS 397 | *********************************************/ 398 | 399 | .reveal .roll { 400 | display: inline-block; 401 | overflow: hidden; 402 | 403 | vertical-align: top; 404 | 405 | -webkit-perspective: 400px; 406 | -moz-perspective: 400px; 407 | -ms-perspective: 400px; 408 | perspective: 400px; 409 | 410 | -webkit-perspective-origin: 50% 50%; 411 | -moz-perspective-origin: 50% 50%; 412 | -ms-perspective-origin: 50% 50%; 413 | perspective-origin: 50% 50%; 414 | } 415 | .reveal .roll:hover { 416 | background: none; 417 | text-shadow: none; 418 | } 419 | .reveal .roll span { 420 | display: block; 421 | position: relative; 422 | padding: 0 2px; 423 | 424 | pointer-events: none; 425 | 426 | -webkit-transition: all 400ms ease; 427 | -moz-transition: all 400ms ease; 428 | -ms-transition: all 400ms ease; 429 | transition: all 400ms ease; 430 | 431 | -webkit-transform-origin: 50% 0%; 432 | -moz-transform-origin: 50% 0%; 433 | -ms-transform-origin: 50% 0%; 434 | transform-origin: 50% 0%; 435 | 436 | -webkit-transform-style: preserve-3d; 437 | -moz-transform-style: preserve-3d; 438 | -ms-transform-style: preserve-3d; 439 | transform-style: preserve-3d; 440 | } 441 | .reveal .roll:hover span { 442 | background: rgba(0,0,0,0.5); 443 | 444 | -webkit-transform: translate3d( 0px, 0px, -45px ) rotateX( 90deg ); 445 | -moz-transform: translate3d( 0px, 0px, -45px ) rotateX( 90deg ); 446 | -ms-transform: translate3d( 0px, 0px, -45px ) rotateX( 90deg ); 447 | transform: translate3d( 0px, 0px, -45px ) rotateX( 90deg ); 448 | } 449 | .reveal .roll span:after { 450 | content: attr(data-title); 451 | 452 | display: block; 453 | position: absolute; 454 | left: 0; 455 | top: 0; 456 | padding: 0 2px; 457 | 458 | color: #fff; 459 | background: #66B360; 460 | 461 | -webkit-transform-origin: 50% 0%; 462 | -moz-transform-origin: 50% 0%; 463 | -ms-transform-origin: 50% 0%; 464 | transform-origin: 50% 0%; 465 | 466 | -webkit-transform: translate3d( 0px, 105%, 0px ) rotateX( -90deg ); 467 | -moz-transform: translate3d( 0px, 105%, 0px ) rotateX( -90deg ); 468 | -ms-transform: translate3d( 0px, 105%, 0px ) rotateX( -90deg ); 469 | transform: translate3d( 0px, 105%, 0px ) rotateX( -90deg ); 470 | } 471 | 472 | 473 | /********************************************* 474 | * SLIDES 475 | *********************************************/ 476 | 477 | .reveal .slides { 478 | position: absolute; 479 | width: 900px; 480 | height: 600px; 481 | 482 | left: 50%; 483 | top: 50%; 484 | margin-left: -450px; 485 | margin-top: -320px; 486 | padding: 20px 0px; 487 | 488 | text-align: center; 489 | 490 | -webkit-transition: -webkit-perspective .4s ease; 491 | -moz-transition: -moz-perspective .4s ease; 492 | -ms-transition: -ms-perspective .4s ease; 493 | -o-transition: -o-perspective .4s ease; 494 | transition: perspective .4s ease; 495 | 496 | -webkit-perspective: 600px; 497 | -moz-perspective: 600px; 498 | -ms-perspective: 600px; 499 | perspective: 600px; 500 | 501 | -webkit-perspective-origin: 50% 25%; 502 | -moz-perspective-origin: 50% 25%; 503 | -ms-perspective-origin: 50% 25%; 504 | perspective-origin: 50% 25%; 505 | } 506 | 507 | .reveal .slides>section, 508 | .reveal .slides>section>section { 509 | display: none; 510 | position: absolute; 511 | width: 100%; 512 | min-height: 600px; 513 | 514 | z-index: 10; 515 | 516 | -webkit-transform-style: preserve-3d; 517 | -moz-transform-style: preserve-3d; 518 | -ms-transform-style: preserve-3d; 519 | transform-style: preserve-3d; 520 | 521 | -webkit-transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 522 | -moz-transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 523 | -ms-transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 524 | -o-transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 525 | transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); 526 | } 527 | 528 | .reveal .slides>section.present { 529 | display: block; 530 | z-index: 11; 531 | opacity: 1; 532 | } 533 | 534 | 535 | /********************************************* 536 | * DEFAULT TRANSITION 537 | *********************************************/ 538 | 539 | .reveal .slides>section.past { 540 | display: block; 541 | opacity: 0; 542 | 543 | -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); 544 | -moz-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); 545 | -ms-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); 546 | transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); 547 | } 548 | .reveal .slides>section.future { 549 | display: block; 550 | opacity: 0; 551 | 552 | -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); 553 | -moz-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); 554 | -ms-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); 555 | transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); 556 | } 557 | 558 | .reveal .slides>section>section.past { 559 | display: block; 560 | opacity: 0; 561 | 562 | -webkit-transform: translate3d(0, -50%, 0) rotateX(70deg) translate3d(0, -50%, 0); 563 | -moz-transform: translate3d(0, -50%, 0) rotateX(70deg) translate3d(0, -50%, 0); 564 | -ms-transform: translate3d(0, -50%, 0) rotateX(70deg) translate3d(0, -50%, 0); 565 | transform: translate3d(0, -50%, 0) rotateX(70deg) translate3d(0, -50%, 0); 566 | } 567 | .reveal .slides>section>section.future { 568 | display: block; 569 | opacity: 0; 570 | 571 | -webkit-transform: translate3d(0, 50%, 0) rotateX(-70deg) translate3d(0, 50%, 0); 572 | -moz-transform: translate3d(0, 50%, 0) rotateX(-70deg) translate3d(0, 50%, 0); 573 | -ms-transform: translate3d(0, 50%, 0) rotateX(-70deg) translate3d(0, 50%, 0); 574 | transform: translate3d(0, 50%, 0) rotateX(-70deg) translate3d(0, 50%, 0); 575 | } 576 | 577 | 578 | /********************************************* 579 | * CONCAVE TRANSITION 580 | *********************************************/ 581 | 582 | .reveal.concave .slides>section.past { 583 | -webkit-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); 584 | -moz-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); 585 | -ms-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); 586 | transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); 587 | } 588 | .reveal.concave .slides>section.future { 589 | -webkit-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); 590 | -moz-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); 591 | -ms-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); 592 | transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); 593 | } 594 | 595 | .reveal.concave .slides>section>section.past { 596 | -webkit-transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); 597 | -moz-transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); 598 | -ms-transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); 599 | transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); 600 | } 601 | .reveal.concave .slides>section>section.future { 602 | -webkit-transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); 603 | -moz-transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); 604 | -ms-transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); 605 | transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); 606 | } 607 | 608 | 609 | /********************************************* 610 | * LINEAR TRANSITION 611 | *********************************************/ 612 | 613 | .reveal.linear .slides>section.past { 614 | -webkit-transform: translate(-150%, 0); 615 | -moz-transform: translate(-150%, 0); 616 | -ms-transform: translate(-150%, 0); 617 | -o-transform: translate(-150%, 0); 618 | transform: translate(-150%, 0); 619 | } 620 | .reveal.linear .slides>section.future { 621 | -webkit-transform: translate(150%, 0); 622 | -moz-transform: translate(150%, 0); 623 | -ms-transform: translate(150%, 0); 624 | -o-transform: translate(150%, 0); 625 | transform: translate(150%, 0); 626 | } 627 | 628 | .reveal.linear .slides>section>section.past { 629 | -webkit-transform: translate(0, -150%); 630 | -moz-transform: translate(0, -150%); 631 | -ms-transform: translate(0, -150%); 632 | -o-transform: translate(0, -150%); 633 | transform: translate(0, -150%); 634 | } 635 | .reveal.linear .slides>section>section.future { 636 | -webkit-transform: translate(0, 150%); 637 | -moz-transform: translate(0, 150%); 638 | -ms-transform: translate(0, 150%); 639 | -o-transform: translate(0, 150%); 640 | transform: translate(0, 150%); 641 | } 642 | 643 | /********************************************* 644 | * BOX TRANSITION 645 | *********************************************/ 646 | 647 | .reveal.cube .slides { 648 | margin-top: -350px; 649 | 650 | -webkit-perspective-origin: 50% 25%; 651 | -moz-perspective-origin: 50% 25%; 652 | -ms-perspective-origin: 50% 25%; 653 | perspective-origin: 50% 25%; 654 | 655 | -webkit-perspective: 1300px; 656 | -moz-perspective: 1300px; 657 | -ms-perspective: 1300px; 658 | perspective: 1300px; 659 | } 660 | 661 | .reveal.cube .slides section { 662 | padding: 30px; 663 | 664 | -webkit-backface-visibility: hidden; 665 | -moz-backface-visibility: hidden; 666 | -ms-backface-visibility: hidden; 667 | backface-visibility: hidden; 668 | 669 | -webkit-box-sizing: border-box; 670 | -moz-box-sizing: border-box; 671 | box-sizing: border-box; 672 | } 673 | .reveal.cube .slides section:not(.stack):before { 674 | content: ''; 675 | position: absolute; 676 | display: block; 677 | width: 100%; 678 | height: 100%; 679 | left: 0; 680 | top: 0; 681 | background: #232628; 682 | border-radius: 4px; 683 | 684 | -webkit-transform: translateZ( -20px ); 685 | -moz-transform: translateZ( -20px ); 686 | -ms-transform: translateZ( -20px ); 687 | -o-transform: translateZ( -20px ); 688 | transform: translateZ( -20px ); 689 | } 690 | .reveal.cube .slides section:not(.stack):after { 691 | content: ''; 692 | position: absolute; 693 | display: block; 694 | width: 90%; 695 | height: 30px; 696 | left: 5%; 697 | bottom: 0; 698 | background: none; 699 | z-index: 1; 700 | 701 | border-radius: 4px; 702 | box-shadow: 0px 95px 25px rgba(0,0,0,0.2); 703 | 704 | -webkit-transform: translateZ(-90px) rotateX( 65deg ); 705 | -moz-transform: translateZ(-90px) rotateX( 65deg ); 706 | -ms-transform: translateZ(-90px) rotateX( 65deg ); 707 | -o-transform: translateZ(-90px) rotateX( 65deg ); 708 | transform: translateZ(-90px) rotateX( 65deg ); 709 | } 710 | 711 | .reveal.cube .slides>section.stack { 712 | padding: 0; 713 | background: none; 714 | } 715 | 716 | .reveal.cube .slides>section.past { 717 | -webkit-transform-origin: 100% 0%; 718 | -moz-transform-origin: 100% 0%; 719 | -ms-transform-origin: 100% 0%; 720 | transform-origin: 100% 0%; 721 | 722 | -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg); 723 | -moz-transform: translate3d(-100%, 0, 0) rotateY(-90deg); 724 | -ms-transform: translate3d(-100%, 0, 0) rotateY(-90deg); 725 | transform: translate3d(-100%, 0, 0) rotateY(-90deg); 726 | } 727 | 728 | .reveal.cube .slides>section.future { 729 | -webkit-transform-origin: 0% 0%; 730 | -moz-transform-origin: 0% 0%; 731 | -ms-transform-origin: 0% 0%; 732 | transform-origin: 0% 0%; 733 | 734 | -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg); 735 | -moz-transform: translate3d(100%, 0, 0) rotateY(90deg); 736 | -ms-transform: translate3d(100%, 0, 0) rotateY(90deg); 737 | transform: translate3d(100%, 0, 0) rotateY(90deg); 738 | } 739 | 740 | .reveal.cube .slides>section>section.past { 741 | -webkit-transform-origin: 0% 100%; 742 | -moz-transform-origin: 0% 100%; 743 | -ms-transform-origin: 0% 100%; 744 | transform-origin: 0% 100%; 745 | 746 | -webkit-transform: translate3d(0, -100%, 0) rotateX(90deg); 747 | -moz-transform: translate3d(0, -100%, 0) rotateX(90deg); 748 | -ms-transform: translate3d(0, -100%, 0) rotateX(90deg); 749 | transform: translate3d(0, -100%, 0) rotateX(90deg); 750 | } 751 | 752 | .reveal.cube .slides>section>section.future { 753 | -webkit-transform-origin: 0% 0%; 754 | -moz-transform-origin: 0% 0%; 755 | -ms-transform-origin: 0% 0%; 756 | transform-origin: 0% 0%; 757 | 758 | -webkit-transform: translate3d(0, 100%, 0) rotateX(-90deg); 759 | -moz-transform: translate3d(0, 100%, 0) rotateX(-90deg); 760 | -ms-transform: translate3d(0, 100%, 0) rotateX(-90deg); 761 | transform: translate3d(0, 100%, 0) rotateX(-90deg); 762 | } 763 | 764 | 765 | /********************************************* 766 | * PAGE TRANSITION 767 | *********************************************/ 768 | 769 | .reveal.page .slides { 770 | margin-top: -350px; 771 | 772 | -webkit-perspective-origin: 50% 50%; 773 | -moz-perspective-origin: 50% 50%; 774 | -ms-perspective-origin: 50% 50%; 775 | perspective-origin: 50% 50%; 776 | 777 | -webkit-perspective: 3000px; 778 | -moz-perspective: 3000px; 779 | -ms-perspective: 3000px; 780 | perspective: 3000px; 781 | } 782 | 783 | .reveal.page .slides section { 784 | padding: 30px; 785 | 786 | -webkit-box-sizing: border-box; 787 | -moz-box-sizing: border-box; 788 | box-sizing: border-box; 789 | } 790 | .reveal.page .slides section.past { 791 | z-index: 12; 792 | } 793 | .reveal.page .slides section:not(.stack):before { 794 | content: ''; 795 | position: absolute; 796 | display: block; 797 | width: 100%; 798 | height: 100%; 799 | left: 0; 800 | top: 0; 801 | background: #232628; 802 | 803 | -webkit-transform: translateZ( -20px ); 804 | -moz-transform: translateZ( -20px ); 805 | -ms-transform: translateZ( -20px ); 806 | -o-transform: translateZ( -20px ); 807 | transform: translateZ( -20px ); 808 | } 809 | .reveal.page .slides section:not(.stack):after { 810 | content: ''; 811 | position: absolute; 812 | display: block; 813 | width: 90%; 814 | height: 30px; 815 | left: 5%; 816 | bottom: 0; 817 | background: none; 818 | z-index: 1; 819 | 820 | border-radius: 4px; 821 | box-shadow: 0px 95px 25px rgba(0,0,0,0.2); 822 | 823 | -webkit-transform: translateZ(-90px) rotateX( 65deg ); 824 | } 825 | 826 | .reveal.page .slides>section.stack { 827 | padding: 0; 828 | background: none; 829 | } 830 | 831 | .reveal.page .slides>section.past { 832 | -webkit-transform-origin: 0% 0%; 833 | -moz-transform-origin: 0% 0%; 834 | -ms-transform-origin: 0% 0%; 835 | transform-origin: 0% 0%; 836 | 837 | -webkit-transform: translate3d(-40%, 0, 0) rotateY(-80deg); 838 | -moz-transform: translate3d(-40%, 0, 0) rotateY(-80deg); 839 | -ms-transform: translate3d(-40%, 0, 0) rotateY(-80deg); 840 | transform: translate3d(-40%, 0, 0) rotateY(-80deg); 841 | } 842 | 843 | .reveal.page .slides>section.future { 844 | -webkit-transform-origin: 100% 0%; 845 | -moz-transform-origin: 100% 0%; 846 | -ms-transform-origin: 100% 0%; 847 | transform-origin: 100% 0%; 848 | 849 | -webkit-transform: translate3d(0, 0, 0); 850 | -moz-transform: translate3d(0, 0, 0); 851 | -ms-transform: translate3d(0, 0, 0); 852 | transform: translate3d(0, 0, 0); 853 | } 854 | 855 | .reveal.page .slides>section>section.past { 856 | -webkit-transform-origin: 0% 0%; 857 | -moz-transform-origin: 0% 0%; 858 | -ms-transform-origin: 0% 0%; 859 | transform-origin: 0% 0%; 860 | 861 | -webkit-transform: translate3d(0, -40%, 0) rotateX(80deg); 862 | -moz-transform: translate3d(0, -40%, 0) rotateX(80deg); 863 | -ms-transform: translate3d(0, -40%, 0) rotateX(80deg); 864 | transform: translate3d(0, -40%, 0) rotateX(80deg); 865 | } 866 | 867 | .reveal.page .slides>section>section.future { 868 | -webkit-transform-origin: 0% 100%; 869 | -moz-transform-origin: 0% 100%; 870 | -ms-transform-origin: 0% 100%; 871 | transform-origin: 0% 100%; 872 | 873 | -webkit-transform: translate3d(0, 0, 0); 874 | -moz-transform: translate3d(0, 0, 0); 875 | -ms-transform: translate3d(0, 0, 0); 876 | transform: translate3d(0, 0, 0); 877 | } 878 | 879 | 880 | /********************************************* 881 | * NEON THEME 882 | *********************************************/ 883 | 884 | .reveal.neon a, 885 | .reveal.neon a:hover, 886 | .reveal.neon .controls a.enabled { 887 | color: #5de048; 888 | } 889 | 890 | .reveal.neon .progress span, 891 | .reveal.neon .roll span:after { 892 | background: #5de048; 893 | } 894 | 895 | .reveal.neon a.image:hover img { 896 | border-color: #5de048; 897 | } 898 | 899 | 900 | /********************************************* 901 | * Strikethrough for Title 902 | *********************************************/ 903 | .insert-container { 904 | -webkit-perspective: 600px; 905 | -moz-perspective: 600px; 906 | -ms-perspective: 600px; 907 | perspective: 600px; 908 | -webkit-transform-style: preserve-3d; 909 | -moz-transform-style: preserve-3d; 910 | -ms-transform-style: preserve-3d; 911 | transform-style: preserve-3d; 912 | } 913 | .insert { 914 | color: #f77 !important; 915 | display: block; 916 | width: auto; 917 | position: absolute; 918 | -webkit-transform: translateX(13.2em) translateY(-.8em) rotateX(5deg) rotateY(5deg) rotateZ(10deg); 919 | -moz-transform: translateX(13.2em) translateY(-.8em) rotateX(5deg) rotateY(5deg) rotateZ(10deg); 920 | -ms-transform: translateX(13.2em) translateY(-.8em) rotateX(5deg) rotateY(5deg) rotateZ(10deg); 921 | transform: translateX(13.2em) translateY(-.8em) rotateX(5deg) rotateY(5deg) rotateZ(10deg); 922 | } 923 | .insert:before { 924 | content: '^'; 925 | color: #f77 !important; 926 | display: block; 927 | font-size: 1.8em; 928 | -webkit-transform: translateY(.4em); 929 | -moz-transform: translateY(.4em); 930 | -ms-transform: translateY(.4em); 931 | transform: translateY(.4em); 932 | } 933 | 934 | /********************************************* 935 | * OVERVIEW 936 | *********************************************/ 937 | 938 | .reveal.overview .slides { 939 | -webkit-perspective: 700px; 940 | -moz-perspective: 700px; 941 | -ms-perspective: 700px; 942 | perspective: 700px; 943 | } 944 | 945 | .reveal.overview .slides section { 946 | padding: 20px 0; 947 | opacity: 1; 948 | cursor: pointer; 949 | background: rgba(0,0,0,0.1); 950 | } 951 | .reveal.overview .slides section .fragment { 952 | opacity: 1; 953 | } 954 | .reveal.overview .slides section:after, 955 | .reveal.overview .slides section:before { 956 | display: none !important; 957 | } 958 | .reveal.overview .slides section>section { 959 | opacity: 1; 960 | cursor: pointer; 961 | } 962 | .reveal.overview .slides section:hover { 963 | background: rgba(0,0,0,0.3); 964 | } 965 | 966 | .reveal.overview .slides section.present { 967 | background: rgba(0,0,0,0.3); 968 | } 969 | .reveal.overview .slides>section.stack { 970 | background: none; 971 | padding: 0; 972 | } 973 | 974 | 975 | /********************************************* 976 | * FALLBACK 977 | *********************************************/ 978 | 979 | .no-transforms { 980 | overflow-y: auto; 981 | } 982 | 983 | .no-transforms .slides section { 984 | -webkit-transform: none; 985 | -moz-transform: none; 986 | -ms-transform: none; 987 | transform: none; 988 | 989 | display: block!important; 990 | opacity: 1!important; 991 | position: relative!important; 992 | } 993 | 994 | 995 | /********************************************* 996 | * DEFAULT STATES 997 | *********************************************/ 998 | 999 | .state-background { 1000 | position: absolute; 1001 | width: 100%; 1002 | height: 100%; 1003 | 1004 | background: rgb(255,255,255); /* Old browsers */ 1005 | background: -moz-radial-gradient(center, ellipse cover, rgba(255,255,255,1) 0%, rgba(242,242,242,1) 100%); /* FF3.6+ */ 1006 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(242,242,242,1))); /* Chrome,Safari4+ */ 1007 | background: -webkit-radial-gradient(center, ellipse cover, rgba(255,255,255,1) 0%,rgba(242,242,242,1) 100%); /* Chrome10+,Safari5.1+ */ 1008 | background: -o-radial-gradient(center, ellipse cover, rgba(255,255,255,1) 0%,rgba(242,242,242,1) 100%); /* Opera 12+ */ 1009 | background: -ms-radial-gradient(center, ellipse cover, rgba(255,255,255,1) 0%,rgba(242,242,242,1) 100%); /* IE10+ */ 1010 | background: radial-gradient(ellipse at center, rgba(255,255,255,1) 0%,rgba(242,242,242,1) 100%); /* W3C */ 1011 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#f2f2f2',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 1012 | 1013 | -webkit-transition: background 800ms ease; 1014 | -moz-transition: background 800ms ease; 1015 | -ms-transition: background 800ms ease; 1016 | -o-transition: background 800ms ease; 1017 | transition: background 800ms ease; 1018 | } 1019 | .alert .state-background { 1020 | background: rgba( 200, 50, 30, 0.6 ); 1021 | } 1022 | .alert .reveal * { 1023 | color: #fff; 1024 | } 1025 | .soothe .state-background { 1026 | background: rgba( 50, 200, 90, 0.4 ); 1027 | } 1028 | .soothe .reveal * { 1029 | color: #fff; 1030 | } 1031 | .blackout .state-background { 1032 | background: rgba( 0, 0, 0, 0.6 ); 1033 | } 1034 | .blackout .reveal * { 1035 | color: #fff; 1036 | } 1037 | .bluesy .state-background { 1038 | background: rgba( 0, 0, 75, 0.5 ); 1039 | } 1040 | .bluesy .reveal * { 1041 | color: #fff; 1042 | } 1043 | 1044 | 1045 | 1046 | 1047 | -------------------------------------------------------------------------------- /css/print.css: -------------------------------------------------------------------------------- 1 | /* Default Print Stylesheet Template 2 | by Rob Glazebrook of CSSnewbie.com 3 | Last Updated: June 4, 2008 4 | 5 | Feel free (nay, compelled) to edit, append, and 6 | manipulate this file as you see fit. */ 7 | 8 | 9 | /* SECTION 1: Set default width, margin, float, and 10 | background. This prevents elements from extending 11 | beyond the edge of the printed page, and prevents 12 | unnecessary background images from printing */ 13 | body { 14 | background: #fff url(none); 15 | font-size: 13pt; 16 | width: auto; 17 | height: auto; 18 | border: 0; 19 | margin: 0 5%; 20 | padding: 0; 21 | float: none !important; 22 | overflow: visible; 23 | } 24 | html { 25 | background: #fff; 26 | width: auto; 27 | height: auto; 28 | overflow: visible; 29 | } 30 | 31 | /* SECTION 2: Remove any elements not needed in print. 32 | This would include navigation, ads, sidebars, etc. */ 33 | .nestedarrow, 34 | .controls a, 35 | .reveal .progress span, 36 | .reveal.overview { 37 | display:none; 38 | } 39 | 40 | /* SECTION 3: Set body font face, size, and color. 41 | Consider using a serif font for readability. */ 42 | body, p, td, li, div, a { 43 | font-size: 13pt; 44 | font-family: Georgia, "Times New Roman", Times, serif !important; 45 | color: #000; 46 | } 47 | 48 | /* SECTION 4: Set heading font face, sizes, and color. 49 | Diffrentiate your headings from your body text. 50 | Perhaps use a large sans-serif for distinction. */ 51 | h1,h2,h3,h4,h5,h6 { 52 | color: #000!important; 53 | height: auto; 54 | line-height: normal; 55 | font-family: Georgia, "Times New Roman", Times, serif !important; 56 | text-shadow: 0 0 0 #000 !important; 57 | text-align: left; 58 | letter-spacing: normal; 59 | } 60 | /* Need to reduce the size of the fonts for printing */ 61 | h1 { font-size: 26pt !important; } 62 | h2 { font-size: 22pt !important; } 63 | h3 { font-size: 20pt !important; } 64 | h4 { font-size: 20pt !important; font-variant: small-caps; } 65 | h5 { font-size: 19pt !important; } 66 | h6 { font-size: 18pt !important; font-style: italic; } 67 | 68 | /* SECTION 5: Make hyperlinks more usable. 69 | Ensure links are underlined, and consider appending 70 | the URL to the end of the link for usability. */ 71 | a:link, 72 | a:visited { 73 | color: #000 !important; 74 | font-weight: bold; 75 | text-decoration: underline; 76 | } 77 | .reveal a:link:after, 78 | .reveal a:visited:after { 79 | content: " (" attr(href) ") "; 80 | color: #222 !important; 81 | font-size: 90%; 82 | } 83 | 84 | 85 | /* SECTION 6: more reveal.js specific additions by @skypanther */ 86 | ul, ol, div, p { 87 | visibility: visible; 88 | position: static; 89 | width: auto; 90 | height: auto; 91 | display: block; 92 | overflow: visible; 93 | margin: auto; 94 | text-align: left !important; 95 | } 96 | .reveal .slides { 97 | position: static; 98 | width: auto; 99 | height: auto; 100 | 101 | left: auto; 102 | top: auto; 103 | margin-left: auto; 104 | margin-top: auto; 105 | padding: auto; 106 | 107 | overflow: visible; 108 | display: block; 109 | 110 | text-align: center; 111 | -webkit-perspective: none; 112 | -moz-perspective: none; 113 | -ms-perspective: none; 114 | perspective: none; 115 | 116 | -webkit-perspective-origin: 50% 50%; /* there isn't a none/auto value but 50-50 is the default */ 117 | -moz-perspective-origin: 50% 50%; 118 | -ms-perspective-origin: 50% 50%; 119 | perspective-origin: 50% 50%; 120 | } 121 | .reveal .slides>section, .reveal .slides>section>section, 122 | .reveal .slides>section.past, .reveal .slides>section.future, 123 | .reveal.linear .slides>section, .reveal.linear .slides>section>section, 124 | .reveal.linear .slides>section.past, .reveal.linear .slides>section.future { 125 | 126 | visibility: visible; 127 | position: static; 128 | width: 90%; 129 | height: auto; 130 | display: block; 131 | overflow: visible; 132 | 133 | left: 0%; 134 | top: 0%; 135 | margin-left: 0px; 136 | margin-top: 0px; 137 | padding: 20px 0px; 138 | 139 | opacity: 1; 140 | 141 | -webkit-transform-style: flat; 142 | -moz-transform-style: flat; 143 | -ms-transform-style: flat; 144 | transform-style: flat; 145 | 146 | -webkit-transform: none; 147 | -moz-transform: none; 148 | -ms-transform: none; 149 | transform: none; 150 | } 151 | .reveal section { 152 | page-break-after: always !important; 153 | display: block !important; 154 | } 155 | .reveal section.stack { 156 | page-break-after: avoid !important; 157 | } 158 | .reveal section .fragment { 159 | opacity: 1 !important; 160 | } 161 | .reveal section img { 162 | display: block; 163 | margin: 15px 0px; 164 | background: rgba(255,255,255,1); 165 | border: 1px solid #666; 166 | box-shadow: none; 167 | } -------------------------------------------------------------------------------- /css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | /* HTML5BP: 52 | These selection declarations have to be separate. 53 | No text-shadow: twitter.com/miketaylr/status/12228805301 54 | Also: hot pink. */ 55 | ::-moz-selection{ background: #66B360; color:#fff; text-shadow: none; } 56 | ::selection { background:#66B360; color:#fff; text-shadow: none; } 57 | 58 | -------------------------------------------------------------------------------- /css/webfonts/BryantWebMedium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/css/webfonts/BryantWebMedium.eot -------------------------------------------------------------------------------- /css/webfonts/BryantWebMedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/css/webfonts/BryantWebMedium.ttf -------------------------------------------------------------------------------- /css/webfonts/BryantWebMedium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/css/webfonts/BryantWebMedium.woff -------------------------------------------------------------------------------- /examples/casper/drupal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Testing a demo of Drupal. Your test environment MUST exist at the URL in 4 | * the `host` property — The script will log in and check for various 5 | * features in Drupal core. This demo was inspired by a similar script for 6 | * a Wordpress site. The original script was written by Henrique Vicente. 7 | * 8 | * NOTE: this file assumes that Drupal's "clean urls" are enabled. If the 9 | * URLs are in a format such as "?q=node/add/page" instead of "/node/add/page" 10 | * the clean URLs need to be enabled before tests that match URLs can pass. 11 | * 12 | * @see https://github.com/henvic/phantom-casper-simple-talk/blob/master/wordpress.js 13 | */ 14 | 15 | // Set up variables to visit a URL and log in. 16 | var config = { 17 | 'host': 'http://casper-drupal.test', 18 | 'form': { 19 | 'name': 'admin', 20 | 'pass': 'admin' 21 | } 22 | }; 23 | 24 | // In one of the tests we want to set some content up, then test it in the 25 | // following block of code. Setting this content up as a global variable helps 26 | // keep the testing code DRY and reliable. 27 | var nodeContents = { 28 | 'title': 'Hello, World!', 29 | 'body[und][0][value]': 'This content was added by CasperJS!' 30 | }; 31 | 32 | // Define the suite of tests and give it the following properties: 33 | // - Title, which shows up before any of the pass/fails. 34 | // - Number of tests, must be changed as you add tests. 35 | // - suite(), which contains all of your tests. 36 | // 37 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#begin 38 | casper.test.begin('Testing Drupal demo site', 8, function suite(test) { 39 | 40 | // casper.start() always wraps your first action. The first argument should 41 | // be the URL of the page you want to test. Instead of being hard-coded, ours 42 | // comes from the config object we defined above. 43 | // 44 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#start 45 | casper.start(config.host, function() { 46 | 47 | // casper.fill() allows you to populate and submit forms. 48 | // 49 | // The first argument is any selector that can uniquely identify your form. 50 | // 51 | // The second argument is an object containing the data you want to submit 52 | // using this form. Our config.form variable was defined at the beginning of 53 | // this file. Each property name of the object you supply should be identical 54 | // to the `name` attr on the form you're filling out. 55 | // 56 | // The third argument is a boolean that tells Casper whether the form should 57 | // be submitted automatically. There are other ways of submitting forms, 58 | // such as finding the submit button and running the click() operation on it. 59 | // 60 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#fill 61 | casper.fill('form#user-login-form', config.form, true); 62 | 63 | // You can output comments at any point in your script. Sometimes it's good 64 | // to add a message when you're attempting an operation that might take a 65 | // few moments to respond, such as this login attempt. 66 | test.comment('⌚️ Logging in...'); 67 | }); 68 | 69 | // casper.then() allows us to wait until previous tests and actions are 70 | // completed before moving on to the next steps. This is useful for many 71 | // situations and authenticated sessions are a prime candidate, since we 72 | // cannot perform any further actions if we failed to authenticate. 73 | // 74 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#then 75 | casper.then(function() { 76 | 77 | // test.assertHttpStatus() determines which HTTP response code was sent from 78 | // the server. All of the tests in this file are checking for 200, which is 79 | // a normal, successful response. But there's nothing stopping someone from 80 | // intentionally checking for 403, 404, or even 500 errors from the server. 81 | // 82 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#asserthttpstatus 83 | test.assertHttpStatus(200, "Authentication successful"); 84 | }) 85 | 86 | casper.then(function() { 87 | // Now that we're logged in, check for the `logged-in` class that is appended 88 | // to the tag for all authenticated Drupal traffic. 89 | // 90 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertexists 91 | test.assertExists('body.logged-in', 'Drupal class for logged-in users was found.'); 92 | 93 | // We want to browse the content list that is available to Drupal admins, 94 | // so we are clicking the Content link in the admin toolbar. The next series 95 | // of steps assumes that Overlay module is enabled, which is the default for 96 | // the Standard installation profile in Drupal 7. 97 | // 98 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#click 99 | this.click('#toolbar-link-admin-content'); 100 | 101 | // Log the click to the console so we know why it's pausing momentarily. 102 | test.comment('⌚️ Clicking the Content admin link...'); 103 | }); 104 | 105 | // Now that we've authenticated and loaded the content page, it's time to look 106 | // for some content. 107 | casper.then(function() { 108 | 109 | // First we see if the Overlay updated the page title correctly. The first 110 | // argument is a regex that is run against tag. The second argument 111 | // is optional and can be used to provide a more descriptive test result. 112 | // 113 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#asserttitlematch 114 | test.assertTitleMatch(/^Content.*/, 'Overlay changed page title to begin with the word "Content"'); 115 | 116 | // We also check that the URL is updated as expected. For science. Arguments 117 | // are the same as test.assertTitleMatch() 118 | // 119 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#asserturlmatch 120 | test.assertUrlMatch(/node#overlay=admin\/content/, 'Overlay updated the URL to /node#overlay=admin/content'); 121 | 122 | // Instead of clicking another link, we want to load the node/add/page page 123 | // without our dear friend, the Overlay. casper.open() simply loads a new 124 | // URL. We put this at the end of the code block and use casper.then() to 125 | // ensure that the next tests are run after the page is finished opening. 126 | // 127 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#open 128 | this.open(config.host + '/node/add/page'); 129 | 130 | // Log this action to the console as an informational courtesy to the user. 131 | test.comment('⌚️ Adding Basic page...'); 132 | }); 133 | 134 | // With our fresh node/add page, we want to populate and submit another form. 135 | casper.then(function() { 136 | 137 | // Check that the page loaded properly. 138 | test.assertHttpStatus(200, 'Opened the node/add/page page.'); 139 | 140 | // Look for the node form that we want to populate. 141 | test.assertExists('form#page-node-form', 'Found the node/add/page form.'); 142 | 143 | // Populate the form with the content we prepared at the beginning of this 144 | // file. As the notes above explain, we do not want to repeat the values we 145 | // submit here in the next test, because if we update these values later and 146 | // forget to change both simultaneously, then it will appear that the site 147 | // broke when it is actually our test that is broken. 148 | casper.fill('form#page-node-form', nodeContents, true); 149 | 150 | // Once again, report that we're saving the node to keep the user informed. 151 | test.comment('⌚️ Saving new node...'); 152 | }); 153 | 154 | // Drupal automatically redirects to the published node on success. With the 155 | // form submitted, we check to see what the published content looks like. 156 | casper.then(function() { 157 | 158 | // Check the page title of the published node. Since we don't want to hard- 159 | // code any of the test values, we use the `new RegExp` syntax which allows 160 | // the regex to contain variables. 161 | test.assertTitleMatch(new RegExp(nodeContents.title), 'Our custom title was found on the published page.'); 162 | 163 | // Now we do some very light screen scraping to find the text that we added 164 | // to the body field of this node. Since Drupal 7 has jQuery 1.4.4 out of 165 | // the box, it is safe to rely on it to extract our text. 166 | // 167 | // test.assertEvalEquals() allows us to execute arbitrary code and compare 168 | // the results with a pre-defined value. The first argument is a function 169 | // containing our code that will be evaluated in the context of the testing 170 | // environment. The second argument (which is sourced from our variable at 171 | // the beginning of the file) is the expected result. The third argument is 172 | // optional and allows us to provide a more descriptive test result. 173 | // 174 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertevalequals 175 | test.assertEvalEquals(function () { 176 | return jQuery('.node-page .content p').text(); 177 | }, nodeContents['body[und][0][value]'], 'Our custom text was found on the published page.'); 178 | }); 179 | 180 | // This code runs all the tests that we defined above. 181 | // 182 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#done 183 | casper.run(function () { 184 | test.done(); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /examples/casper/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "casperjs": { 6 | "version": "1.1.4", 7 | "resolved": "https://registry.npmjs.org/casperjs/-/casperjs-1.1.4.tgz", 8 | "integrity": "sha1-6wH07YWsUgqPTZMrTap00+d7x0Y=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/casper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "casperjs": "^1.1.4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/casper/picturefill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Testing to see if Picturefill selects the right source at multiple 4 | * viewport sizes. 5 | */ 6 | 7 | // Define the suite of tests and give it the following properties: 8 | // - Title, which shows up before any of the pass/fails. 9 | // - Number of tests, must be changed as you add tests. 10 | // - suite(), which contains all of your tests. 11 | // 12 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#begin 13 | casper.test.begin('Testing Picturefill', 5, function suite(test) { 14 | test.comment('⌚️ Opening https://scottjehl.github.io/picturefill/examples/demo-02.html'); 15 | 16 | // casper.start() always wraps your first action. The first argument should 17 | // be the URL of the page you want to test. 18 | // 19 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#start 20 | casper.start('https://scottjehl.github.io/picturefill/examples/demo-02.html', function () { 21 | 22 | // First, we look for a <picture> element. The first argument is a query 23 | // selector, like jQuery or document.querySelectorAll(). Any <picture> tags 24 | // on the page are found by using the 'picture' selector. 25 | // 26 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertexists 27 | test.assertExists('picture', '<picture> element found.'); 28 | 29 | // Now verify that the <picture> tag has two <source> tags. This is 30 | // another query selector. In a more complex test you will need to write 31 | // a more specific selector. This will find all <source> tags within all 32 | // <picture> tags in your document. This demo only has one <picture> tag 33 | // so the simple selector works fine. 34 | // 35 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertelementcount 36 | test.assertElementCount('picture > source', 2); 37 | 38 | // Before running any viewport-specific tests, set the viewport to 320x480. 39 | // PhantomJS has a default viewport of 400x300 and CasperJS does not alter 40 | // this default, so it's always a good idea to explicitly set the viewport 41 | // to your desired dimensions. 42 | // 43 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#viewport 44 | this.viewport(320, 480); 45 | }); 46 | 47 | // casper.then() allows us to wait until previous tests and actions are 48 | // completed before moving on to the next steps. This is useful for many 49 | // situations and viewport resizing is one of them, since scripts like 50 | // Picturefill have to respond to the resize. 51 | // 52 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#then 53 | casper.then(function() { 54 | 55 | // With the viewport resized, look for the src of the <img> and make sure it 56 | // is set to the value we expect. In this example we are testing the smaller 57 | // viewport first, so medium.jpg should be found since the Picturefill markup 58 | // declares it as default. 59 | // 60 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertevalequals 61 | test.assertEvalEquals(function () { 62 | return document.querySelectorAll('picture img')[0].getAttribute('src').match('medium.jpg').toString(); 63 | }, 'medium.jpg', 'medium.jpg found using 320x480 viewport.'); 64 | }); 65 | 66 | // Resize the viewport again 67 | casper.then(function() { 68 | this.viewport(960, 640); 69 | }); 70 | 71 | // With the viewport resized to 960px, check and see if the large.jpg is 72 | // now contained within the <img> src, since the demo markup specifies an 73 | // 800px breakpoint for this image. 74 | casper.then(function() { 75 | test.assertEvalEquals(function () { 76 | return document.querySelectorAll('picture img')[0].getAttribute('src').match('large.jpg').toString(); 77 | }, 'large.jpg', 'large.jpg found using 960x640 viewport.'); 78 | }); 79 | 80 | // Resize the viewport again 81 | casper.then(function() { 82 | this.viewport(1280, 1024); 83 | }); 84 | 85 | // Finally, with the 1280x1024 viewport, check to see if the <img> is now 86 | // using the extralarge.jpg source which was specified for viewports larger 87 | // than 1000px wide. 88 | casper.then(function() { 89 | test.assertEvalEquals(function () { 90 | return document.querySelectorAll('picture img')[0].getAttribute('src').match('extralarge.jpg').toString(); 91 | }, 'extralarge.jpg', 'extralarge.jpg found using 1280x1024 viewport.'); 92 | }); 93 | 94 | // This code runs all the tests that we defined above. 95 | // 96 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#done 97 | casper.run(function () { 98 | test.done(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /examples/casper/test-js-code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Using CasperJS to run tests on various libraries. 4 | */ 5 | 6 | // Define the suite of tests and give it the following properties: 7 | // - Title, which shows up before any of the pass/fails. 8 | // - Number of tests, must be changed as you add tests. 9 | // - suite(), which contains all of your tests. 10 | // 11 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#begin 12 | casper.test.begin('Basic code tests for jQuery and Modernizr', 3, function suite(test) { 13 | // You can output comments at any point in your script. Sometimes it's good 14 | // to add a message when you're attempting an operation that might take a 15 | // few moments to respond, such as this page load. 16 | test.comment('⌚️ Opening https://rupl.github.io/frontend-testing/examples/targets/test-js-code.html'); 17 | 18 | // casper.start() always wraps your first action. The first argument should 19 | // be the URL of the page you want to test. 20 | // 21 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#start 22 | casper.start('https://rupl.github.io/frontend-testing/examples/targets/test-js-code.html', function () { 23 | 24 | // Look at a specific property on the jQuery object to check its version. 25 | // 26 | // assertEvalEquals provides an easy way for us to test JavaScript variables 27 | // within the test environment. Any code within the assertEvalEquals() code 28 | // block is considered to be part of the web page, as if we are typing into 29 | // the JS console of the fully-loaded page. 30 | // 31 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertevalequals 32 | test.assertEvalEquals(function () { 33 | // In jQuery 3+ the version number is accompanied by some build info, so 34 | // our return value splits that extra data off. 35 | return jQuery.fn.jquery.split(' ')[0]; 36 | }, '3.3.1', 'jQuery 3.3.1 was found.'); 37 | 38 | // Look at a specific property on the Modernizr object to check its version. 39 | test.assertEvalEquals(function () { 40 | return Modernizr._version; 41 | }, '3.6.0', 'Modernizr 3.6.0 was found.'); 42 | 43 | // Check for required Modernizr tests. This is NOT testing the output! We 44 | // are only testing whether Modernizr is working, not what its output is. 45 | test.assertEvalEquals(function () { 46 | // An array of strings. Each one represents a required test that must be 47 | // found within Modernizr. That value might be true or false, but as long 48 | // as the type is Boolean we ultimately do not care. 49 | var requiredTests = [ 50 | 'csstransformslevel2', 51 | 'serviceworker', 52 | ]; 53 | 54 | // confirmedTests will end up as a single boolean. First, we map the 55 | // requiredTests array and see if the global Modernizr object contains a 56 | // boolean for each value we entered into requiredTests. 57 | // 58 | // Note: we're not testing the value of the Modernizr test result, but 59 | // verifying that the test was present. Each browser will have a different 60 | // result, so it's not very useful to verify Modernizr's output in a test 61 | // environment. 62 | var confirmedTests = requiredTests.map(function(thisTest) { 63 | // This should return the type of data (boolean), not the boolean value. 64 | return typeof Modernizr[thisTest]; 65 | }).every(function(thisType) { 66 | // Each data type will be verified to be boolean, and the result of the 67 | // .every() function will only be true if all the values resolved to true. 68 | // If even one of the values is not boolean, .every() returns false. 69 | return thisType === 'boolean'; 70 | }); 71 | 72 | // Return the result of our computation to Casper. The return value of 73 | // confirmedTests is ultimately what decides whether this test is reported 74 | // as PASS or FAIL. All of the other computation beforehand was internal. 75 | return confirmedTests; 76 | }, true, 'Required Modernizr tests are all present.'); 77 | }); 78 | 79 | // This code runs all the tests that we defined above. 80 | // 81 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#done 82 | casper.run(function () { 83 | test.done(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /examples/casper/user-actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Simulating user actions with CasperJS. This script explores the ability to 4 | * use Casper for navigation just like a user would: clicking the page and 5 | * entering text to submit a form. 6 | */ 7 | 8 | // This object will hold all of the config/content that Casper needs to supply. 9 | var config = { 10 | url: 'https://rupl.github.io/frontend-testing/examples/targets/test-user-actions-p1.html', 11 | form: { 12 | "name": "Chris Ruppel", 13 | "email": "me@example.com", 14 | "project-title": "CasperJS Test Project", 15 | "project-desc": "CasperJS Test Project Description", 16 | "project-type": "freelance", 17 | } 18 | }; 19 | 20 | 21 | // Define the suite of tests and give it the following properties: 22 | // - Title, which shows up before any of the pass/fails. 23 | // - Number of tests, must be changed as you add tests. 24 | // - suite(), which contains all of your tests. 25 | // 26 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#begin 27 | casper.test.begin('Testing navigation and forms', 7, function suite(test) { 28 | test.comment('⌚️ Loading ' + config.url); 29 | 30 | // casper.start() always wraps your first action. The first argument should 31 | // be the URL of the page you want to test. Instead of being hard-coded, ours 32 | // comes from the config object we defined above. 33 | // 34 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#start 35 | casper.start(config.url, function() { 36 | 37 | // casper.click() fires a click event on a particular element. In this case 38 | // we're clicking on the main logo of the site. 39 | // 40 | // The only argument needed is a selector. Be careful to be specific when 41 | // initiating an action like this. For instance, a selector such as plain 42 | // "a" would not be specific enough. 43 | // 44 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#click 45 | this.click('nav li:first-child a'); 46 | 47 | // Log the click to the console so we know why it's pausing momentarily. 48 | test.comment('⌚️ Clicking the "Contact us" link...'); 49 | }); 50 | 51 | // casper.then() allows us to wait until previous tests and actions are 52 | // completed before moving on to the next steps. This is useful for many 53 | // situations and authenticated sessions are a prime candidate, since we 54 | // cannot perform any further actions if we failed to authenticate. 55 | // 56 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#then 57 | casper.then(function () { 58 | // test.assertUrlMatch() allows us to run a regular expression against the 59 | // current URL that Casper has loaded. 60 | // 61 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#asserturlmatch 62 | test.assertUrlMatch(/test-user-actions-p2/, 'New location is ' + this.getCurrentUrl()); 63 | 64 | // Report that we're attempting to use keyboard nav. 65 | test.comment('⌚️ Using keyboard nav to visit contact page...'); 66 | 67 | // casper.sendKeys() allows us to simulate pressing one or more keys on the 68 | // keyboard. You can use this to trigger a JS event listener, enter text 69 | // into an <input> or element with `contenteditable` attribute, or use it 70 | // to test keyboard navigation. 71 | // 72 | // Our use-case is triggering the `accesskey` property on one of the menu 73 | // items, selecting the <body> works just fine. If you want to test a 74 | // specific <input> or editable element, the function can accept a more 75 | // specific selector. 76 | // 77 | // In this case we're pressing a combo: Ctrl+Alt+C, which is the way to use 78 | // keyboard navigation in PhantomJS. We do this passing the options object 79 | // to sendKeys() and specifying a `modifiers` value. You can find all the 80 | // possible modifier keys in the second docs link. 81 | // 82 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#sendkeys 83 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#options 84 | this.sendKeys('body', 'c', {modifiers: 'ctrl+alt'}); 85 | }); 86 | 87 | casper.then(function () { 88 | // Check the URL again to confirm navigation. Look earlier in this file for 89 | // explanation and docs link for test.assertUrlMatch(). 90 | test.assertUrlMatch(/test-user-actions-p3/, 'New location is ' + this.getCurrentUrl()); 91 | 92 | // casper.fill() allows us to quickly fill out a form with a minimal amount 93 | // of code. If you can write a JSON object, you already know how to fill 94 | // forms in Casper. 95 | // 96 | // @see http://casperjs.readthedocs.org/en/latest/modules/casper.html#fill 97 | casper.fill('#contact', config.form, false); 98 | }); 99 | 100 | casper.then(function () { 101 | // Look for the information we just populated within the form. 102 | // 103 | // assertEvalEquals provides an easy way for us to test JavaScript within 104 | // the test environment. Any code within the assertEvalEquals() code block 105 | // is considered to be part of the web page, as if we are typing into the 106 | // browser's JS console of the fully-loaded page. 107 | // 108 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#assertevalequals 109 | test.assertEvalEquals(function () { 110 | return $('#contact [name="name"]').val(); 111 | }, config.form.name, 'The name was filled out.'); 112 | 113 | // Check the email. 114 | test.assertEvalEquals(function () { 115 | return $('#contact [name="email"]').val(); 116 | }, config.form.email, 'The email was filled out.'); 117 | 118 | // Check the project title. 119 | test.assertEvalEquals(function () { 120 | return $('#contact [name="project-title"]').val(); 121 | }, config.form['project-title'], 'The project title was filled out.'); 122 | 123 | // Check the project description. 124 | test.assertEvalEquals(function () { 125 | return $('#contact [name="project-desc"]').val(); 126 | }, config.form['project-desc'], 'The project description was filled out.'); 127 | 128 | // Check the project type. 129 | test.assertEvalEquals(function () { 130 | return $('#contact [name="project-type"]').val(); 131 | }, config.form['project-type'], 'A project type was selected.'); 132 | }); 133 | 134 | // This code runs all the tests that we defined above. 135 | // 136 | // @see http://casperjs.readthedocs.org/en/latest/modules/tester.html#done 137 | casper.run(function () { 138 | test.done(); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /examples/grunt/devperf/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | devperf: { 4 | options: { 5 | urls: [ 6 | 'http://gruntjs.com' 7 | ], 8 | resultsFolder: './reports/' 9 | } 10 | } 11 | }); 12 | 13 | grunt.loadNpmTasks('grunt-devperf'); 14 | grunt.registerTask('default', ['devperf']); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/grunt/devperf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "~0.4.4", 4 | "grunt-devperf": "~0.2.5" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/grunt/jshint/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | jshint: { 8 | files: { 9 | src: ['*.js'] 10 | }, 11 | options: { 12 | es3: true // Compatability for IE 6/7/8 13 | } 14 | }, 15 | 16 | watch: { 17 | js: { 18 | files: ['*.js'], 19 | tasks: ['jshint'] 20 | } 21 | } 22 | }); 23 | 24 | grunt.loadNpmTasks('grunt-contrib-watch'); 25 | grunt.loadNpmTasks('grunt-contrib-jshint'); 26 | 27 | grunt.registerTask('default', ['watch']); 28 | }; 29 | -------------------------------------------------------------------------------- /examples/grunt/jshint/example.js: -------------------------------------------------------------------------------- 1 | // This file has syntax errors that will trigger feedback from JSHint 2 | 3 | $('.my-sample-selector').hide() 4 | 5 | if (my_var == false) { 6 | console.log('hello!'); 7 | } 8 | 9 | var es3_object_broken = { 10 | key1: 'value1', 11 | key2: 'value2', 12 | } 13 | -------------------------------------------------------------------------------- /examples/grunt/jshint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jshint-example", 3 | "version": "0.0.1", 4 | "devDependencies": { 5 | "grunt": "~0.4.1", 6 | "grunt-contrib-watch": "~0.5.3", 7 | "grunt-contrib-jshint": "~0.6.4" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/grunt/pagespeed/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // The settings var includes your PageSpeed API Key. To use yours, first go to 2 | // the following URL and generate/lookup your Google PageSpeed API key: 3 | // 4 | // @see https://code.google.com/apis/console/ 5 | // 6 | // Now make a file called settings.json in the same dir as this Gruntfile and 7 | // include an object like this: 8 | // 9 | // { 10 | // "key": "your-google-pagespeed-api-key" 11 | // } 12 | var settings = require('./settings.json'); 13 | 14 | // This is the normal Gruntfile 15 | module.exports = function(grunt) { 16 | grunt.initConfig({ 17 | pkg: grunt.file.readJSON('package.json'), 18 | 19 | pagespeed: { 20 | desktop: { 21 | url: "http://gruntjs.com", 22 | locale: "en_US", 23 | strategy: "desktop", 24 | threshold: 85 25 | }, 26 | mobile: { 27 | url: "http://gruntjs.com", 28 | locale: "en_US", 29 | strategy: "mobile", 30 | threshold: 85 31 | }, 32 | options: { 33 | key: settings.key 34 | } 35 | } 36 | }); 37 | 38 | grunt.loadNpmTasks('grunt-pagespeed'); 39 | 40 | grunt.registerTask('default', ['pagespeed:desktop']); 41 | }; 42 | -------------------------------------------------------------------------------- /examples/grunt/pagespeed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "~0.4.1", 4 | "grunt-pagespeed": "0.1.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/grunt/perfbudget/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // The settings var includes your WebPageTest API Key. To use yours, first go to 2 | // the following URL to get a key: 3 | // 4 | // @see http://www.webpagetest.org/getkey.php 5 | // 6 | // Now make a file called settings.json in the same dir as this 7 | // Gruntfile and include an object like this: 8 | // 9 | // { 10 | // "key": "your-web-page-test-api-key" 11 | // } 12 | var settings = require('./settings.json'); 13 | 14 | // This is the normal Gruntfile 15 | module.exports = function(grunt) { 16 | grunt.initConfig({ 17 | pkg: grunt.file.readJSON('package.json'), 18 | 19 | perfbudget: { 20 | default: { 21 | // See all options here: 22 | // https://github.com/tkadlec/grunt-perfbudget#options 23 | options: { 24 | url: 'https://iamcarrico.com', 25 | key: settings.key, 26 | budget: { 27 | render: 800, 28 | requests: 10 29 | } 30 | } 31 | } 32 | } 33 | }); 34 | 35 | grunt.loadNpmTasks('grunt-perfbudget'); 36 | 37 | grunt.registerTask('default', ['perfbudget']); 38 | }; 39 | -------------------------------------------------------------------------------- /examples/grunt/perfbudget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "~0.4.1", 4 | "grunt-perfbudget": "~0.1.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/grunt/phantomas/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | phantomas: { 8 | default: { 9 | options: { 10 | indexPath: './reports/', 11 | options: {}, 12 | url: 'http://gruntjs.com/' 13 | } 14 | }, 15 | screenshot: { 16 | options: { 17 | indexPath: './reports/', 18 | options: { 19 | 'screenshot': 'screenshots/sample-' + Date.now() + '.png' 20 | }, 21 | url: 'http://gruntjs.com/' 22 | } 23 | }, 24 | requests: { 25 | options: { 26 | indexPath: './reports/', 27 | options: { 28 | 'assert-requests': 20 29 | }, 30 | url: 'http://gruntjs.com/' 31 | } 32 | }, 33 | } 34 | }); 35 | 36 | grunt.loadNpmTasks('grunt-phantomas'); 37 | 38 | grunt.registerTask('default', ['phantomas:default']); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/grunt/phantomas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "~0.4.1", 4 | "grunt-phantomas": "~0.7.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/targets/README.md: -------------------------------------------------------------------------------- 1 | # Testing Targets for CasperJS 2 | 3 | This directory holds static assets that will be tested by CasperJS. If you're viewing this on GitHub, go one directory up and peer into the `casper` directory. 4 | -------------------------------------------------------------------------------- /examples/targets/modernizr-3.6.0-custom.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.6.0 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-csstransformslevel2-serviceworker-setclasses !*/ 3 | !function(e,n,t){function r(e,n){return typeof e===n}function o(){var e,n,t,o,s,i,l;for(var a in w)if(w.hasOwnProperty(a)){if(e=[],n=w[a],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(o=r(n.fn,"function")?n.fn():n.fn,s=0;s<e.length;s++)i=e[s],l=i.split("."),1===l.length?Modernizr[l[0]]=o:(!Modernizr[l[0]]||Modernizr[l[0]]instanceof Boolean||(Modernizr[l[0]]=new Boolean(Modernizr[l[0]])),Modernizr[l[0]][l[1]]=o),C.push((o?"":"no-")+l.join("-"))}}function s(e){var n=_.className,t=Modernizr._config.classPrefix||"";if(x&&(n=n.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(r,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),x?_.className.baseVal=n:_.className=n)}function i(e,n){return!!~(""+e).indexOf(n)}function l(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):x?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function a(e){return e.replace(/([a-z])-([a-z])/g,function(e,n,t){return n+t.toUpperCase()}).replace(/^-/,"")}function u(e,n){return function(){return e.apply(n,arguments)}}function f(e,n,t){var o;for(var s in e)if(e[s]in n)return t===!1?e[s]:(o=n[e[s]],r(o,"function")?u(o,t||n):o);return!1}function c(e){return e.replace(/([A-Z])/g,function(e,n){return"-"+n.toLowerCase()}).replace(/^ms-/,"-ms-")}function d(n,t,r){var o;if("getComputedStyle"in e){o=getComputedStyle.call(e,n,t);var s=e.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(s){var i=s.error?"error":"log";s[i].call(s,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!t&&n.currentStyle&&n.currentStyle[r];return o}function p(){var e=n.body;return e||(e=l(x?"svg":"body"),e.fake=!0),e}function m(e,t,r,o){var s,i,a,u,f="modernizr",c=l("div"),d=p();if(parseInt(r,10))for(;r--;)a=l("div"),a.id=o?o[r]:f+(r+1),c.appendChild(a);return s=l("style"),s.type="text/css",s.id="s"+f,(d.fake?d:c).appendChild(s),d.appendChild(c),s.styleSheet?s.styleSheet.cssText=e:s.appendChild(n.createTextNode(e)),c.id=f,d.fake&&(d.style.background="",d.style.overflow="hidden",u=_.style.overflow,_.style.overflow="hidden",_.appendChild(d)),i=t(c,e),d.fake?(d.parentNode.removeChild(d),_.style.overflow=u,_.offsetHeight):c.parentNode.removeChild(c),!!i}function v(n,r){var o=n.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(c(n[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var s=[];o--;)s.push("("+c(n[o])+":"+r+")");return s=s.join(" or "),m("@supports ("+s+") { #modernizr { position: absolute; } }",function(e){return"absolute"==d(e,null,"position")})}return t}function y(e,n,o,s){function u(){c&&(delete N.style,delete N.modElem)}if(s=r(s,"undefined")?!1:s,!r(o,"undefined")){var f=v(e,o);if(!r(f,"undefined"))return f}for(var c,d,p,m,y,g=["modernizr","tspan","samp"];!N.style&&g.length;)c=!0,N.modElem=l(g.shift()),N.style=N.modElem.style;for(p=e.length,d=0;p>d;d++)if(m=e[d],y=N.style[m],i(m,"-")&&(m=a(m)),N.style[m]!==t){if(s||r(o,"undefined"))return u(),"pfx"==n?m:!0;try{N.style[m]=o}catch(h){}if(N.style[m]!=y)return u(),"pfx"==n?m:!0}return u(),!1}function g(e,n,t,o,s){var i=e.charAt(0).toUpperCase()+e.slice(1),l=(e+" "+P.join(i+" ")+i).split(" ");return r(n,"string")||r(n,"undefined")?y(l,n,o,s):(l=(e+" "+z.join(i+" ")+i).split(" "),f(l,n,t))}function h(e,n,r){return g(e,t,t,n,r)}var C=[],w=[],S={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){w.push({name:e,fn:n,options:t})},addAsyncTest:function(e){w.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=S,Modernizr=new Modernizr;var _=n.documentElement,x="svg"===_.nodeName.toLowerCase();Modernizr.addTest("serviceworker","serviceWorker"in navigator);var b="Moz O ms Webkit",P=S._config.usePrefixes?b.split(" "):[];S._cssomPrefixes=P;var z=S._config.usePrefixes?b.toLowerCase().split(" "):[];S._domPrefixes=z;var E={elem:l("modernizr")};Modernizr._q.push(function(){delete E.elem});var N={style:E.elem.style};Modernizr._q.unshift(function(){delete N.style}),S.testAllProps=g,S.testAllProps=h,Modernizr.addTest("csstransformslevel2",function(){return h("translate","45px",!0)}),o(),s(C),delete S.addTest,delete S.addAsyncTest;for(var T=0;T<Modernizr._q.length;T++)Modernizr._q[T]();e.Modernizr=Modernizr}(window,document); 4 | -------------------------------------------------------------------------------- /examples/targets/test-js-code.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html dir="ltr" lang="en-US"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Testing JS Code with CasperJS 6 | 16 | 17 | 18 |

CasperJS — Testing JS code

19 |

This page exists solely to offer a stable testing target for an example CasperJS script.

20 |

Learn more about automated frontend testing.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/targets/test-user-actions-p1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing JS Code with CasperJS 6 | 16 | 17 | 18 |

CasperJS — Testing user actions

19 |

This page exists to offer a stable testing target for an example CasperJS script.

20 |

Learn more about automated frontend testing.

21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/targets/test-user-actions-p2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing JS Code with CasperJS 6 | 16 | 17 | 18 |

CasperJS — Testing user actions

19 |

This page exists to offer a stable testing target for an example CasperJS script.

20 |

Learn more about automated frontend testing.

21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/targets/test-user-actions-p3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testing JS Code with CasperJS 6 | 35 | 36 | 37 |

CasperJS — Testing user actions

38 |

This page exists to offer a stable testing target for an example CasperJS script.

39 |

Learn more about automated frontend testing.

40 |
41 | 42 |
43 |
44 | Contact information 45 | 46 | 47 |
48 |
49 |
50 | Project information 51 |
52 | 53 | 54 |
56 |

This is a sample form which has no submit button.

57 |
58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/wraith/configs/demo.yaml: -------------------------------------------------------------------------------- 1 | #Headless browser option 2 | browser: 3 | webkit: "phantomjs" 4 | # gecko: "slimerjs" 5 | 6 | #If you want to have multiple snapping files, set the file name here 7 | snap_file: "javascript/snap.js" 8 | 9 | # Type the name of the directory that shots will be stored in 10 | directory: 11 | - 'shots' 12 | 13 | # Add only 2 domains, key will act as a label 14 | domains: 15 | live: "http://fourkitchens.com" 16 | dev: "http://localhost:4000" 17 | 18 | #Type screen widths below, here are a couple of examples 19 | screen_widths: 20 | - 320 21 | - 768 22 | - 1280 23 | 24 | #Type page URL paths below, here are a couple of examples 25 | paths: 26 | home: / 27 | 28 | # If you don't want to name the paths explicitly you can use a yaml 29 | # collection as follows, and names will be derived by replacing / with _ 30 | # 31 | # paths: 32 | # - /imghp 33 | # - /maps 34 | 35 | #Amount of fuzz ImageMagick will use 36 | fuzz: '20%' 37 | 38 | #Set the number of days to keep the site spider file 39 | spider_days: 40 | - 10 41 | -------------------------------------------------------------------------------- /examples/wraith/javascript/snap.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var page = require('webpage').create(); 3 | var fs = require('fs'); 4 | 5 | if (system.args.length === 3) { 6 | console.log('Usage: snap.js '); 7 | phantom.exit(); 8 | } 9 | 10 | var url = system.args[1]; 11 | var image_name = system.args[3]; 12 | var view_port_width = system.args[2]; 13 | var current_requests = 0; 14 | var last_request_timeout; 15 | var final_timeout; 16 | 17 | 18 | page.viewportSize = { width: view_port_width, height: 1500}; 19 | page.settings = { loadImages: true, javascriptEnabled: true }; 20 | 21 | // If you want to use additional phantomjs commands, place them here 22 | page.settings.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.17'; 23 | 24 | // You can place custom headers here, example below. 25 | // page.customHeaders = { 26 | 27 | // 'X-Candy-OVERRIDE': 'https://api.live.bbc.co.uk/' 28 | 29 | // }; 30 | 31 | // If you want to set a cookie, just add your details below in the following way. 32 | 33 | // phantom.addCookie({ 34 | // 'name': 'ckns_policy', 35 | // 'value': '111', 36 | // 'domain': '.bbc.co.uk' 37 | // }); 38 | // phantom.addCookie({ 39 | // 'name': 'locserv', 40 | // 'value': '1#l1#i=6691484:n=Oxford+Circus:h=e@w1#i=8:p=London@d1#1=l:2=e:3=e:4=2@n1#r=40', 41 | // 'domain': '.bbc.co.uk' 42 | // }); 43 | 44 | page.onResourceRequested = function(req) { 45 | current_requests += 1; 46 | }; 47 | 48 | page.onResourceReceived = function(res) { 49 | if (res.stage === 'end') { 50 | current_requests -= 1; 51 | debounced_render(); 52 | } 53 | }; 54 | 55 | page.open(url, function(status) { 56 | if (status !== 'success') { 57 | console.log('Error with page ' + url); 58 | phantom.exit(); 59 | } 60 | }); 61 | 62 | 63 | function debounced_render() { 64 | clearTimeout(last_request_timeout); 65 | clearTimeout(final_timeout); 66 | 67 | // If there's no more ongoing resource requests, wait for 1 second before 68 | // rendering, just in case the page kicks off another request 69 | if (current_requests < 1) { 70 | clearTimeout(final_timeout); 71 | last_request_timeout = setTimeout(function() { 72 | console.log('Snapping ' + url + ' at width ' + view_port_width); 73 | page.render(image_name); 74 | phantom.exit(); 75 | }, 1000); 76 | } 77 | 78 | // Sometimes, straggling requests never make it back, in which 79 | // case, timeout after 5 seconds and render the page anyway 80 | final_timeout = setTimeout(function() { 81 | console.log('Snapping ' + url + ' at width ' + view_port_width); 82 | page.render(image_name); 83 | phantom.exit(); 84 | }, 5000); 85 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/favicon.ico -------------------------------------------------------------------------------- /img/4k-logo-square-500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/4k-logo-square-500px.png -------------------------------------------------------------------------------- /img/browserstack-screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/browserstack-screenshots.png -------------------------------------------------------------------------------- /img/delaypackage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/delaypackage.gif -------------------------------------------------------------------------------- /img/grunt-devperf-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/grunt-devperf-graph.png -------------------------------------------------------------------------------- /img/grunt-perfbudget-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/grunt-perfbudget-output.png -------------------------------------------------------------------------------- /img/grunt-phantomas-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/grunt-phantomas-graph.png -------------------------------------------------------------------------------- /img/jshint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/jshint.png -------------------------------------------------------------------------------- /img/timing-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/timing-overview.png -------------------------------------------------------------------------------- /img/visitor-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/visitor-flow.png -------------------------------------------------------------------------------- /img/wraith-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rupl/frontend-testing/d565f924cca5aa51a76cb88a06b325cbb793768c/img/wraith-example.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Automated Frontend Testing 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 |
42 |
43 |

44 |

Automated Frontend Testing


45 |

Or: How to Automate the Process of detecting when you Break Things

46 | 50 |
51 | 52 |
53 |

Why do I need frontend testing?

54 |
55 |
56 |

There are untold number of subtle
errors that can occur on the frontend.

57 |
58 |
    59 |
  • Minor CSS changes that throw things off
  • 60 |
  • Changes to JS files that break things
  • 61 |
  • Aggregates changing when not necessary
  • 62 |
  • Performance regressions

  • 63 |
64 |
65 |
66 |
67 |

Additionally, frontend development is
becoming more critical as the trade matures.

68 |
69 |

We need the same testing abilities
that the backend has had for years.

70 |
71 |
72 |
73 |
    74 |
  • Testing page load times
  • 75 |
  • Testing render speeds
  • 76 |
  • Sticking to a performance budget
  • 77 |
  • Verifying visual changes
  • 78 |
  • Accountability for code changes
  • 79 |
80 |
81 |
82 | 83 |
84 |
85 |

workflow_alter()

86 |
87 |

In order to deliver the best, fastest site possible, we have to change our development processes.

88 |
89 |
Performance is not a checklist, it's a
continuous process.
Ilya Grigorik
90 |
91 |
…don’t take measures without measuring them.
Maximiliano Firtman
92 |
93 |
94 | 95 |
96 |

YES

97 |
98 |

The slides AND code are on GitHub, I will tweet the link. Yes, you may take code from the slides and use it for any purpose :)

99 |
100 |

This presentation is using a fresh clone of these slides, using the many examples I provided in the repo.

101 |
102 | 103 |
104 |
105 |
106 |

Functional testing

107 |
108 |
109 |

CasperJS

110 |

Casper allows for scripted actions to be tested. It uses PhantomJS under the hood.

111 |
112 |
    113 |
  • Run the same test with multiple screen sizes.
  • 114 |
  • Test complex features or components.
  • 115 |
  • Automate complex user actions.
  • 116 |
  • Test content creation, transactions, other features.
  • 117 |
  • Keep an eye on problematic pages.
  • 118 |
119 |
120 |
121 |

Test frontend components

122 |
123 |
# test the canonical picturefill demo
124 | $ casperjs test picturefill.js
125 |
126 |

Read the blog post or watch a screencast
describing this test in detail.

127 |
128 |
129 |

Simulate user actions

130 |
131 |
# test simple user actions like clicking,
132 | # keyboard navigation, filling forms
133 | $ casperjs test user-actions.js
134 |
135 |

Read the blog post describing this test in detail.

136 |
137 |
138 |

Test author
workflow in Drupal

139 |
140 |
# test a Drupal demo site, log in, add content
141 | $ casperjs test drupal.js
142 |
143 |

Read the blog post or watch a video.

144 |
145 |
146 |
147 |

More examples

148 |
149 |

Keep checking our blog post series on the Four Kitchens blog to learn more about CasperJS.

150 |
151 |
152 | 153 |
154 |
155 |
156 |

Performance Testing

157 |
158 |
159 |

Automating PageSpeed

160 |
161 |

Google has a service called PageSpeed Insights that grades your site and boils down tons of factors into a "speed index"

162 |
163 |

164 | Testing sites can be automated. Get your API key first. 165 |

166 |

167 |
168 |
169 |

grunt-pagespeed

170 |
171 |

PageSpeed API is documented quite
thoroughly, but there's also a grunt plugin.

172 |
173 |
# examples/grunt/pagespeed
174 | $ npm install
175 | $ grunt # runs default task
176 | $ grunt pagespeed:mobile # runs mobile task
177 |
178 | View code on GitHub 179 |
180 |
181 |

gulp + pagespeed

182 |
183 |

Una Kravets wrote up an excellent walkthrough for gulp + pagespeed + local development

184 |

185 |

See the Four Kitchens
frontend performance training
for an alternate implementation

186 |
187 |
188 |

Phantomas

189 |
190 |

Phantomas is a PhantomJS-based
web performance metrics tool

191 |
192 |

It gives you loads of data about how the
frontend of your website is performing.

193 |
194 |

The usage guide is extensive.

195 |
196 |
197 |
198 |
# run a basic report
199 | $ phantomas --url http://gruntjs.com
200 | 
201 | # set viewport dimensions, generate images of rendering process
202 | $ phantomas --url http://gruntjs.com --viewport=320x480 --film-strip
203 | 
204 | # assert a test for total number of requests
205 | $ phantomas --url http://gruntjs.com --assert-requests=20
206 | 
207 |
208 |
209 |

grunt-phantomas

210 |
211 |

This grunt plugin is not just a wrapper for running the tool.

212 |
213 |

It provides detailed reports that track your data over time, allowing you to identify trends using dynamic charts that update themselves each time you run the grunt task

214 |
215 |
216 |
217 | Graph displaying a spike in CSS size. 218 |
219 |
220 |
221 |
# examples/grunt/phantomas
222 | $ grunt phantomas:default
223 | 
224 | # run report and generate screenshot
225 | $ grunt phantomas:screenshot
226 | 
227 | # test for certain values. this might cause failure!
228 | $ grunt phantomas:requests
229 |
230 | View code on GitHub 231 |
232 |
233 |

Performance Budgets

234 |
235 |

The idea is simple: performance budgets are just like a monthy expense budget. We should keep track of how fat our sites grow over time.

236 |
237 |

grunt-phantomas has performance budget features that visualize over-budget metrics that you set.

238 |
239 |
240 |

grunt-perfbudget

241 |
242 |

Tim Kadlec, who first suggested performance budgets, released this tool to help teams meet their goals.

243 |
244 |

grunt-perfbudget relies on the immensely useful WebPageTest API to enforce a budget.

245 |
246 |

WebPageTest.org and its API are much more flexible than PhantomJS tools, because it can leverage multiple browsers, geographic locations, and network speeds.

247 |
248 |
249 |
# examples/grunt/perfbudget
250 | $ npm install
251 | 
252 | # run report
253 | $ grunt perfbudget
254 | 
255 | View code on GitHub 256 |
257 |
258 | Output of grunt-perfbudget task 259 | 260 |
261 |
262 | 263 |
264 |
265 |
266 |

CSS Regression testing

267 |
268 |
269 |
270 |

CSS regressions? say it ain't so!

271 |
272 |

Having no scope at all, CSS is
the easiest thing to nudge out of place.

273 |
274 |

It's also easier to prevent than you think.

275 |
276 |
277 |

Wraith

278 |

Wraith is the easiest way to take screenshots of two environments, producing a visual diff.

279 | 280 |
281 |
282 |

Basic usage of Wraith

283 |
284 |
# examples/wraith
285 | $ gem install wraith
286 | 
287 | # run the capture process
288 | $ wraith capture demo
289 | 
290 | # view results in the browser
291 | $ open shots/gallery.html
292 |
293 |
294 |

Multiple tests

295 |
296 |

Wraith handles one comparison per config file.

297 |
298 |

However, it has support for multiple configs, so several config files in one repo allows for multiple comparisons.

299 |
300 |

Read more on GitHub

301 |
302 |
303 | 304 |
305 |
306 |

Automating Tasks with CI

307 |
308 |
309 |

The Basics

310 |
311 |

Everything outlined in this section
requires two key ingredients:

312 |
313 |
    314 |
  • Continuous integration (CI)
  • 315 |
  • Git hooks
  • 316 |
317 |

318 |

Four Kitchens uses Jenkins and GitHub WebHooks
in our workflow, and you can use whatever you wish.

319 |
320 |
321 | 322 |
323 |
324 |

Trigger Jenkins builds
by pushing to GitHub

325 |
326 |

An oldie but goodie: check out our how-to from 2011

327 |
328 |
    329 |
  1. You push to GitHub master branch (or merge PR)
  2. 330 |
  3. GitHub pings your CI server using post-receive hook
  4. 331 |
  5. "Yo Jenkins, the repository was updated!"
  6. 332 |
  7. CI server pulls the new code to your staging area
  8. 333 |
334 |
335 |
336 |

Although fairly simple in application, the post illustrates the basic concepts underlying all tasks involving Jenkins.

337 |
338 |
339 |

Use git hooks to test before pushing code

340 |
341 |

Git can be configured to automatically run
tasks before or after many of its operations.

342 |
343 |

pre-commit hook that runs jshint on your JavaScript.

344 |
345 |

pre-push hook that runs performance tests on your local. Helps enforce performance budgets.

346 |
347 |
348 |

Travis CI


349 |
    350 |
  • Simple config via YAML file
  • 351 |
  • FREE for open-source projects
  • 352 |
353 |

354 |

Example using Four Kitchens' frontend performance training repo.

Click the red × to see Travis log

355 |
356 |
357 | 358 |
359 |
360 |

Automated Testing Services

361 |
362 |
363 |

Ghost Inspector


364 |

If you have a team willing to code up these examples, great! I like writing tests to complement code.


365 |

If not, Ghost Inspector can record a user's actions as they browse normally, and turn those actions into Casper code.


366 |

Ghost Inspector

367 |
368 |
369 | 370 |
371 |
372 |

Further reading

373 |
374 | 380 |
381 | 382 |
383 |
384 |

Thanks for getting this far!

385 |

386 |

hire me for frontend performance consulting

387 |

github.com/rupl

388 |

twitter.com/rupl

389 |

drupal.org/u/rupl

390 |

chris.ruppel ❀ gmail

391 |
392 |
393 | 394 | 395 | 401 | 402 | 403 |
404 | 405 |
406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 449 | 450 | 451 | -------------------------------------------------------------------------------- /js/reveal.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * reveal.js 1.4 3 | * http://lab.hakim.se/reveal-js 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | var Reveal = (function(){ 9 | 10 | var HORIZONTAL_SLIDES_SELECTOR = '.reveal .slides>section', 11 | VERTICAL_SLIDES_SELECTOR = '.reveal .slides>section.present>section', 12 | 13 | IS_TOUCH_DEVICE = !!( 'ontouchstart' in window ), 14 | 15 | // The horizontal and verical index of the currently active slide 16 | indexh = 0, 17 | indexv = 0, 18 | 19 | // Configurations options, can be overridden at initialization time 20 | config = { 21 | controls: true, 22 | progress: false, 23 | history: false, 24 | loop: false, 25 | mouseWheel: true, 26 | rollingLinks: true, 27 | transition: 'default', 28 | theme: 'default' 29 | }, 30 | 31 | // Slides may hold a data-state attribute which we pick up and apply 32 | // as a class to the body. This list contains the combined state of 33 | // all current slides. 34 | state = [], 35 | 36 | // Cached references to DOM elements 37 | dom = {}, 38 | 39 | // Detect support for CSS 3D transforms 40 | supports3DTransforms = document.body.style['WebkitPerspective'] !== undefined || 41 | document.body.style['MozPerspective'] !== undefined || 42 | document.body.style['msPerspective'] !== undefined || 43 | document.body.style['OPerspective'] !== undefined || 44 | document.body.style['perspective'] !== undefined, 45 | 46 | supports2DTransforms = document.body.style['WebkitTransform'] !== undefined || 47 | document.body.style['MozTransform'] !== undefined || 48 | document.body.style['msTransform'] !== undefined || 49 | document.body.style['OTransform'] !== undefined || 50 | document.body.style['transform'] !== undefined, 51 | 52 | // Detect support for elem.classList 53 | supportsClassList = !!document.body.classList; 54 | 55 | // Throttles mouse wheel navigation 56 | mouseWheelTimeout = 0, 57 | 58 | // Delays updates to the URL due to a Chrome thumbnailer bug 59 | writeURLTimeout = 0, 60 | 61 | // Holds information about the currently ongoing touch input 62 | touch = { 63 | startX: 0, 64 | startY: 0, 65 | startSpan: 0, 66 | startCount: 0, 67 | handled: false, 68 | threshold: 40 69 | }; 70 | 71 | 72 | /** 73 | * Starts up the slideshow by applying configuration 74 | * options and binding various events. 75 | */ 76 | function initialize( options ) { 77 | 78 | if( ( !supports2DTransforms && !supports3DTransforms ) || !supportsClassList ) { 79 | document.body.setAttribute( 'class', 'no-transforms' ); 80 | 81 | // If the browser doesn't support core features we won't be 82 | // using JavaScript to control the presentation 83 | return; 84 | } 85 | 86 | // Cache references to DOM elements 87 | dom.wrapper = document.querySelector( '.reveal' ); 88 | dom.progress = document.querySelector( '.reveal .progress' ); 89 | dom.progressbar = document.querySelector( '.reveal .progress span' ); 90 | 91 | if ( config.controls ) { 92 | dom.controls = document.querySelector( '.reveal .controls' ); 93 | dom.controlsLeft = document.querySelector( '.reveal .controls .left' ); 94 | dom.controlsRight = document.querySelector( '.reveal .controls .right' ); 95 | dom.controlsUp = document.querySelector( '.reveal .controls .up' ); 96 | dom.controlsDown = document.querySelector( '.reveal .controls .down' ); 97 | } 98 | 99 | addEventListeners(); 100 | 101 | // Copy options over to our config object 102 | extend( config, options ); 103 | 104 | // Updates the presentation to match the current configuration values 105 | configure(); 106 | 107 | // Read the initial hash 108 | readURL(); 109 | 110 | // Set up hiding of the browser address bar 111 | if( navigator.userAgent.match( /(iphone|ipod|android)/i ) ) { 112 | // Give the page some scrollable overflow 113 | document.documentElement.style.overflow = 'scroll'; 114 | document.body.style.height = '120%'; 115 | 116 | // Events that should trigger the address bar to hide 117 | window.addEventListener( 'load', removeAddressBar, false ); 118 | window.addEventListener( 'orientationchange', removeAddressBar, false ); 119 | } 120 | 121 | } 122 | 123 | function configure() { 124 | // Fall back on the 2D transform theme 'linear' 125 | if( supports3DTransforms === false ) { 126 | config.transition = 'linear'; 127 | } 128 | 129 | if( config.controls && dom.controls ) { 130 | dom.controls.style.display = 'block'; 131 | } 132 | 133 | if( config.progress ) { 134 | dom.progress.style.display = 'block'; 135 | } 136 | 137 | if( config.transition !== 'default' ) { 138 | dom.wrapper.classList.add( config.transition ); 139 | } 140 | 141 | if( config.theme !== 'default' ) { 142 | dom.wrapper.classList.add( config.theme ); 143 | } 144 | 145 | if( config.mouseWheel ) { 146 | document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF 147 | document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); 148 | } 149 | 150 | if( config.rollingLinks ) { 151 | // Add some 3D magic to our anchors 152 | linkify(); 153 | } 154 | } 155 | 156 | function addEventListeners() { 157 | document.addEventListener( 'keydown', onDocumentKeyDown, false ); 158 | document.addEventListener( 'touchstart', onDocumentTouchStart, false ); 159 | document.addEventListener( 'touchmove', onDocumentTouchMove, false ); 160 | document.addEventListener( 'touchend', onDocumentTouchEnd, false ); 161 | window.addEventListener( 'hashchange', onWindowHashChange, false ); 162 | 163 | if ( config.controls && dom.controls ) { 164 | dom.controlsLeft.addEventListener( 'click', preventAndForward( navigateLeft ), false ); 165 | dom.controlsRight.addEventListener( 'click', preventAndForward( navigateRight ), false ); 166 | dom.controlsUp.addEventListener( 'click', preventAndForward( navigateUp ), false ); 167 | dom.controlsDown.addEventListener( 'click', preventAndForward( navigateDown ), false ); 168 | } 169 | } 170 | 171 | function removeEventListeners() { 172 | document.removeEventListener( 'keydown', onDocumentKeyDown, false ); 173 | document.removeEventListener( 'touchstart', onDocumentTouchStart, false ); 174 | document.removeEventListener( 'touchmove', onDocumentTouchMove, false ); 175 | document.removeEventListener( 'touchend', onDocumentTouchEnd, false ); 176 | window.removeEventListener( 'hashchange', onWindowHashChange, false ); 177 | 178 | if ( config.controls && dom.controls ) { 179 | dom.controlsLeft.removeEventListener( 'click', preventAndForward( navigateLeft ), false ); 180 | dom.controlsRight.removeEventListener( 'click', preventAndForward( navigateRight ), false ); 181 | dom.controlsUp.removeEventListener( 'click', preventAndForward( navigateUp ), false ); 182 | dom.controlsDown.removeEventListener( 'click', preventAndForward( navigateDown ), false ); 183 | } 184 | } 185 | 186 | /** 187 | * Extend object a with the properties of object b. 188 | * If there's a conflict, object b takes precedence. 189 | */ 190 | function extend( a, b ) { 191 | for( var i in b ) { 192 | a[ i ] = b[ i ]; 193 | } 194 | } 195 | 196 | /** 197 | * Measures the distance in pixels between point a 198 | * and point b. 199 | * 200 | * @param {Object} a point with x/y properties 201 | * @param {Object} b point with x/y properties 202 | */ 203 | function distanceBetween( a, b ) { 204 | var dx = a.x - b.x, 205 | dy = a.y - b.y; 206 | 207 | return Math.sqrt( dx*dx + dy*dy ); 208 | } 209 | 210 | /** 211 | * Prevents an events defaults behavior calls the 212 | * specified delegate. 213 | * 214 | * @param {Function} delegate The method to call 215 | * after the wrapper has been executed 216 | */ 217 | function preventAndForward( delegate ) { 218 | return function( event ) { 219 | event.preventDefault(); 220 | delegate.call(); 221 | } 222 | } 223 | 224 | /** 225 | * Causes the address bar to hide on mobile devices, 226 | * more vertical space ftw. 227 | */ 228 | function removeAddressBar() { 229 | setTimeout( function() { 230 | window.scrollTo( 0, 1 ); 231 | }, 0 ); 232 | } 233 | 234 | /** 235 | * Handler for the document level 'keydown' event. 236 | * 237 | * @param {Object} event 238 | */ 239 | function onDocumentKeyDown( event ) { 240 | // FFT: Use document.querySelector( ':focus' ) === null 241 | // instead of checking contentEditable? 242 | 243 | // Disregard the event if the target is editable or a 244 | // modifier is present 245 | if ( event.target.contentEditable != 'inherit' || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; 246 | 247 | var triggered = false; 248 | 249 | switch( event.keyCode ) { 250 | // p, page up 251 | case 80: case 33: navigatePrev(); triggered = true; break; 252 | // n, page down 253 | case 78: case 34: navigateNext(); triggered = true; break; 254 | // h, left 255 | case 72: case 37: navigateLeft(); triggered = true; break; 256 | // l, right 257 | case 76: case 39: navigateRight(); triggered = true; break; 258 | // k, up 259 | case 75: case 38: navigateUp(); triggered = true; break; 260 | // j, down 261 | case 74: case 40: navigateDown(); triggered = true; break; 262 | // home 263 | case 36: navigateTo( 0 ); triggered = true; break; 264 | // end 265 | case 35: navigateTo( Number.MAX_VALUE ); triggered = true; break; 266 | // space 267 | case 32: overviewIsActive() ? deactivateOverview() : navigateNext(); triggered = true; break; 268 | // return 269 | case 13: if( overviewIsActive() ) { deactivateOverview(); triggered = true; } break; 270 | } 271 | 272 | if( triggered ) { 273 | event.preventDefault(); 274 | } 275 | else if ( event.keyCode === 27 && supports3DTransforms ) { 276 | if( overviewIsActive() ) { 277 | deactivateOverview(); 278 | } 279 | else { 280 | activateOverview(); 281 | } 282 | 283 | event.preventDefault(); 284 | } 285 | 286 | } 287 | 288 | /** 289 | * Handler for the document level 'touchstart' event, 290 | * enables support for swipe and pinch gestures. 291 | */ 292 | function onDocumentTouchStart( event ) { 293 | touch.startX = event.touches[0].clientX; 294 | touch.startY = event.touches[0].clientY; 295 | touch.startCount = event.touches.length; 296 | 297 | // If there's two touches we need to memorize the distance 298 | // between those two points to detect pinching 299 | if( event.touches.length === 2 ) { 300 | touch.startSpan = distanceBetween( { 301 | x: event.touches[1].clientX, 302 | y: event.touches[1].clientY 303 | }, { 304 | x: touch.startX, 305 | y: touch.startY 306 | } ); 307 | } 308 | } 309 | 310 | /** 311 | * Handler for the document level 'touchmove' event. 312 | */ 313 | function onDocumentTouchMove( event ) { 314 | // Each touch should only trigger one action 315 | if( !touch.handled ) { 316 | var currentX = event.touches[0].clientX; 317 | var currentY = event.touches[0].clientY; 318 | 319 | // If the touch started off with two points and still has 320 | // two active touches; test for the pinch gesture 321 | if( event.touches.length === 2 && touch.startCount === 2 ) { 322 | 323 | // The current distance in pixels between the two touch points 324 | var currentSpan = distanceBetween( { 325 | x: event.touches[1].clientX, 326 | y: event.touches[1].clientY 327 | }, { 328 | x: touch.startX, 329 | y: touch.startY 330 | } ); 331 | 332 | // If the span is larger than the desire amount we've got 333 | // ourselves a pinch 334 | if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) { 335 | touch.handled = true; 336 | 337 | if( currentSpan < touch.startSpan ) { 338 | activateOverview(); 339 | } 340 | else { 341 | deactivateOverview(); 342 | } 343 | } 344 | 345 | } 346 | // There was only one touch point, look for a swipe 347 | else if( event.touches.length === 1 ) { 348 | var deltaX = currentX - touch.startX, 349 | deltaY = currentY - touch.startY; 350 | 351 | if( deltaX > touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { 352 | touch.handled = true; 353 | navigateLeft(); 354 | } 355 | else if( deltaX < -touch.threshold && Math.abs( deltaX ) > Math.abs( deltaY ) ) { 356 | touch.handled = true; 357 | navigateRight(); 358 | } 359 | else if( deltaY > touch.threshold ) { 360 | touch.handled = true; 361 | navigateUp(); 362 | } 363 | else if( deltaY < -touch.threshold ) { 364 | touch.handled = true; 365 | navigateDown(); 366 | } 367 | } 368 | 369 | event.preventDefault(); 370 | } 371 | } 372 | 373 | /** 374 | * Handler for the document level 'touchend' event. 375 | */ 376 | function onDocumentTouchEnd( event ) { 377 | touch.handled = false; 378 | } 379 | 380 | /** 381 | * Handles mouse wheel scrolling, throttled to avoid 382 | * skipping multiple slides. 383 | */ 384 | function onDocumentMouseScroll( event ){ 385 | clearTimeout( mouseWheelTimeout ); 386 | 387 | mouseWheelTimeout = setTimeout( function() { 388 | var delta = event.detail || -event.wheelDelta; 389 | if( delta > 0 ) { 390 | navigateNext(); 391 | } 392 | else { 393 | navigatePrev(); 394 | } 395 | }, 100 ); 396 | } 397 | 398 | /** 399 | * Handler for the window level 'hashchange' event. 400 | * 401 | * @param {Object} event 402 | */ 403 | function onWindowHashChange( event ) { 404 | readURL(); 405 | } 406 | 407 | /** 408 | * Wrap all links in 3D goodness. 409 | */ 410 | function linkify() { 411 | if( supports3DTransforms ) { 412 | var nodes = document.querySelectorAll( '.reveal .slides section a:not(.image)' ); 413 | 414 | for( var i = 0, len = nodes.length; i < len; i++ ) { 415 | var node = nodes[i]; 416 | 417 | if( node.textContent && !node.querySelector( 'img' ) && ( !node.className || !node.classList.contains( node, 'roll' ) ) ) { 418 | node.classList.add( 'roll' ); 419 | node.innerHTML = '' + node.innerHTML + ''; 420 | } 421 | }; 422 | } 423 | } 424 | 425 | /** 426 | * Displays the overview of slides (quick nav) by 427 | * scaling down and arranging all slide elements. 428 | * 429 | * Experimental feature, might be dropped if perf 430 | * can't be improved. 431 | */ 432 | function activateOverview() { 433 | 434 | dom.wrapper.classList.add( 'overview' ); 435 | 436 | var horizontalSlides = Array.prototype.slice.call( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); 437 | 438 | for( var i = 0, len1 = horizontalSlides.length; i < len1; i++ ) { 439 | var hslide = horizontalSlides[i], 440 | htransform = 'translateZ(-2500px) translate(' + ( ( i - indexh ) * 105 ) + '%, 0%)'; 441 | 442 | hslide.setAttribute( 'data-index-h', i ); 443 | hslide.style.display = 'block'; 444 | hslide.style.WebkitTransform = htransform; 445 | hslide.style.MozTransform = htransform; 446 | hslide.style.msTransform = htransform; 447 | hslide.style.OTransform = htransform; 448 | hslide.style.transform = htransform; 449 | 450 | if( !hslide.classList.contains( 'stack' ) ) { 451 | // Navigate to this slide on click 452 | hslide.addEventListener( 'click', onOverviewSlideClicked, true ); 453 | } 454 | 455 | var verticalSlides = Array.prototype.slice.call( hslide.querySelectorAll( 'section' ) ); 456 | 457 | for( var j = 0, len2 = verticalSlides.length; j < len2; j++ ) { 458 | var vslide = verticalSlides[j], 459 | vtransform = 'translate(0%, ' + ( ( j - indexv ) * 105 ) + '%)'; 460 | 461 | vslide.setAttribute( 'data-index-h', i ); 462 | vslide.setAttribute( 'data-index-v', j ); 463 | vslide.style.display = 'block'; 464 | vslide.style.WebkitTransform = vtransform; 465 | vslide.style.MozTransform = vtransform; 466 | vslide.style.msTransform = vtransform; 467 | vslide.style.OTransform = vtransform; 468 | vslide.style.transform = vtransform; 469 | 470 | // Navigate to this slide on click 471 | vslide.addEventListener( 'click', onOverviewSlideClicked, true ); 472 | } 473 | 474 | } 475 | } 476 | 477 | /** 478 | * Exits the slide overview and enters the currently 479 | * active slide. 480 | */ 481 | function deactivateOverview() { 482 | dom.wrapper.classList.remove( 'overview' ); 483 | 484 | var slides = Array.prototype.slice.call( document.querySelectorAll( '.reveal .slides section' ) ); 485 | 486 | for( var i = 0, len = slides.length; i < len; i++ ) { 487 | var element = slides[i]; 488 | 489 | // Resets all transforms to use the external styles 490 | element.style.WebkitTransform = ''; 491 | element.style.MozTransform = ''; 492 | element.style.msTransform = ''; 493 | element.style.OTransform = ''; 494 | element.style.transform = ''; 495 | 496 | element.removeEventListener( 'click', onOverviewSlideClicked ); 497 | } 498 | 499 | slide(); 500 | } 501 | 502 | /** 503 | * Checks if the overview is currently active. 504 | * 505 | * @return {Boolean} true if the overview is active, 506 | * false otherwise 507 | */ 508 | function overviewIsActive() { 509 | return dom.wrapper.classList.contains( 'overview' ); 510 | } 511 | 512 | /** 513 | * Invoked when a slide is and we're in the overview. 514 | */ 515 | function onOverviewSlideClicked( event ) { 516 | // TODO There's a bug here where the event listeners are not 517 | // removed after deactivating the overview. 518 | if( overviewIsActive() ) { 519 | event.preventDefault(); 520 | 521 | deactivateOverview(); 522 | 523 | indexh = this.getAttribute( 'data-index-h' ); 524 | indexv = this.getAttribute( 'data-index-v' ); 525 | 526 | slide(); 527 | } 528 | } 529 | 530 | /** 531 | * Updates one dimension of slides by showing the slide 532 | * with the specified index. 533 | * 534 | * @param {String} selector A CSS selector that will fetch 535 | * the group of slides we are working with 536 | * @param {Number} index The index of the slide that should be 537 | * shown 538 | * 539 | * @return {Number} The index of the slide that is now shown, 540 | * might differ from the passed in index if it was out of 541 | * bounds. 542 | */ 543 | function updateSlides( selector, index ) { 544 | 545 | // Select all slides and convert the NodeList result to 546 | // an array 547 | var slides = Array.prototype.slice.call( document.querySelectorAll( selector ) ), 548 | slidesLength = slides.length; 549 | 550 | if( slidesLength ) { 551 | 552 | // Should the index loop? 553 | if( config.loop ) { 554 | index %= slidesLength; 555 | 556 | if( index < 0 ) { 557 | index = slidesLength + index; 558 | } 559 | } 560 | 561 | // Enforce max and minimum index bounds 562 | index = Math.max( Math.min( index, slidesLength - 1 ), 0 ); 563 | 564 | for( var i = 0; i < slidesLength; i++ ) { 565 | var slide = slides[i]; 566 | 567 | // Optimization; hide all slides that are three or more steps 568 | // away from the present slide 569 | if( overviewIsActive() === false ) { 570 | // The distance loops so that it measures 1 between the first 571 | // and last slides 572 | var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0; 573 | 574 | slide.style.display = distance > 3 ? 'none' : 'block'; 575 | } 576 | 577 | slides[i].classList.remove( 'past' ); 578 | slides[i].classList.remove( 'present' ); 579 | slides[i].classList.remove( 'future' ); 580 | 581 | if( i < index ) { 582 | // Any element previous to index is given the 'past' class 583 | slides[i].classList.add( 'past' ); 584 | } 585 | else if( i > index ) { 586 | // Any element subsequent to index is given the 'future' class 587 | slides[i].classList.add( 'future' ); 588 | } 589 | 590 | // If this element contains vertical slides 591 | if( slide.querySelector( 'section' ) ) { 592 | slides[i].classList.add( 'stack' ); 593 | } 594 | } 595 | 596 | // Mark the current slide as present 597 | slides[index].classList.add( 'present' ); 598 | 599 | // If this slide has a state associated with it, add it 600 | // onto the current state of the deck 601 | var slideState = slides[index].getAttribute( 'data-state' ); 602 | if( slideState ) { 603 | state = state.concat( slideState.split( ' ' ) ); 604 | } 605 | } 606 | else { 607 | // Since there are no slides we can't be anywhere beyond the 608 | // zeroth index 609 | index = 0; 610 | } 611 | 612 | return index; 613 | 614 | } 615 | 616 | /** 617 | * Updates the visual slides to represent the currently 618 | * set indices. 619 | */ 620 | function slide( h, v ) { 621 | // Remember the state before this slide 622 | var stateBefore = state.concat(); 623 | 624 | // Reset the state array 625 | state.length = 0; 626 | 627 | var indexhBefore = indexh, 628 | indexvBefore = indexv; 629 | 630 | // Activate and transition to the new slide 631 | indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); 632 | indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); 633 | 634 | // Apply the new state 635 | stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { 636 | // Check if this state existed on the previous slide. If it 637 | // did, we will avoid adding it repeatedly. 638 | for( var j = 0; j < stateBefore.length; j++ ) { 639 | if( stateBefore[j] === state[i] ) { 640 | stateBefore.splice( j, 1 ); 641 | continue stateLoop; 642 | } 643 | } 644 | 645 | document.documentElement.classList.add( state[i] ); 646 | 647 | // Dispatch custom event matching the state's name 648 | dispatchEvent( state[i] ); 649 | } 650 | 651 | // Clean up the remaints of the previous state 652 | while( stateBefore.length ) { 653 | document.documentElement.classList.remove( stateBefore.pop() ); 654 | } 655 | 656 | // Update progress if enabled 657 | if( config.progress ) { 658 | dom.progressbar.style.width = ( indexh / ( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length - 1 ) ) * window.innerWidth + 'px'; 659 | } 660 | 661 | // Close the overview if it's active 662 | if( overviewIsActive() ) { 663 | activateOverview(); 664 | } 665 | 666 | updateControls(); 667 | 668 | clearTimeout( writeURLTimeout ); 669 | writeURLTimeout = setTimeout( writeURL, 1500 ); 670 | 671 | // Only fire if the slide index is different from before 672 | if( indexh !== indexhBefore || indexv !== indexvBefore ) { 673 | // Query all horizontal slides in the deck 674 | var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); 675 | 676 | // Find the previous and current horizontal slides 677 | var previousHorizontalSlide = horizontalSlides[ indexhBefore ], 678 | currentHorizontalSlide = horizontalSlides[ indexh ]; 679 | 680 | // Query all vertical slides inside of the previous and current horizontal slides 681 | var previousVerticalSlides = previousHorizontalSlide.querySelectorAll( 'section' ); 682 | currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); 683 | 684 | // Dispatch an event notifying observers of the change in slide 685 | dispatchEvent( 'slidechanged', { 686 | // Include the current indices in the event 687 | 'indexh': indexh, 688 | 'indexv': indexv, 689 | 690 | // Passes direct references to the slide HTML elements, attempts to find 691 | // a vertical slide and falls back on the horizontal parent 692 | 'previousSlide': previousVerticalSlides[ indexvBefore ] || previousHorizontalSlide, 693 | 'currentSlide': currentVerticalSlides[ indexv ] || currentHorizontalSlide 694 | } ); 695 | } 696 | } 697 | 698 | /** 699 | * Updates the state and link pointers of the controls. 700 | */ 701 | function updateControls() { 702 | if ( !config.controls || !dom.controls ) { 703 | return; 704 | } 705 | 706 | var routes = availableRoutes(); 707 | 708 | // Remove the 'enabled' class from all directions 709 | [ dom.controlsLeft, dom.controlsRight, dom.controlsUp, dom.controlsDown ].forEach( function( node ) { 710 | node.classList.remove( 'enabled' ); 711 | } ) 712 | 713 | if( routes.left ) dom.controlsLeft.classList.add( 'enabled' ); 714 | if( routes.right ) dom.controlsRight.classList.add( 'enabled' ); 715 | if( routes.up ) dom.controlsUp.classList.add( 'enabled' ); 716 | if( routes.down ) dom.controlsDown.classList.add( 'enabled' ); 717 | } 718 | 719 | /** 720 | * Determine what available routes there are for navigation. 721 | * 722 | * @return {Object} containing four booleans: left/right/up/down 723 | */ 724 | function availableRoutes() { 725 | var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); 726 | var verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); 727 | 728 | return { 729 | left: indexh > 0, 730 | right: indexh < horizontalSlides.length - 1, 731 | up: indexv > 0, 732 | down: indexv < verticalSlides.length - 1 733 | }; 734 | } 735 | 736 | /** 737 | * Reads the current URL (hash) and navigates accordingly. 738 | */ 739 | function readURL() { 740 | // Break the hash down to separate components 741 | var bits = window.location.hash.slice(2).split('/'); 742 | 743 | // Read the index components of the hash 744 | indexh = parseInt( bits[0] ) || 0 ; 745 | indexv = parseInt( bits[1] ) || 0 ; 746 | 747 | navigateTo( indexh, indexv ); 748 | } 749 | 750 | /** 751 | * Updates the page URL (hash) to reflect the current 752 | * state. 753 | */ 754 | function writeURL() { 755 | if( config.history ) { 756 | var url = '/'; 757 | 758 | // Only include the minimum possible number of components in 759 | // the URL 760 | if( indexh > 0 || indexv > 0 ) url += indexh; 761 | if( indexv > 0 ) url += '/' + indexv; 762 | 763 | window.location.hash = url; 764 | } 765 | } 766 | 767 | /** 768 | * Dispatches an event of the specified type from the 769 | * reveal DOM element. 770 | */ 771 | function dispatchEvent( type, properties ) { 772 | var event = document.createEvent( "HTMLEvents", 1, 2 ); 773 | event.initEvent( type, true, true ); 774 | extend( event, properties ); 775 | dom.wrapper.dispatchEvent( event ); 776 | } 777 | 778 | /** 779 | * Navigate to the next slide fragment. 780 | * 781 | * @return {Boolean} true if there was a next fragment, 782 | * false otherwise 783 | */ 784 | function nextFragment() { 785 | // Vertical slides: 786 | if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { 787 | var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); 788 | if( verticalFragments.length ) { 789 | verticalFragments[0].classList.add( 'visible' ); 790 | 791 | // Notify subscribers of the change 792 | dispatchEvent( 'fragmentshown', { fragment: verticalFragments[0] } ); 793 | return true; 794 | } 795 | } 796 | // Horizontal slides: 797 | else { 798 | var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' ); 799 | if( horizontalFragments.length ) { 800 | horizontalFragments[0].classList.add( 'visible' ); 801 | 802 | // Notify subscribers of the change 803 | dispatchEvent( 'fragmentshown', { fragment: horizontalFragments[0] } ); 804 | return true; 805 | } 806 | } 807 | 808 | return false; 809 | } 810 | 811 | /** 812 | * Navigate to the previous slide fragment. 813 | * 814 | * @return {Boolean} true if there was a previous fragment, 815 | * false otherwise 816 | */ 817 | function previousFragment() { 818 | // Vertical slides: 819 | if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) { 820 | var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' ); 821 | if( verticalFragments.length ) { 822 | verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' ); 823 | 824 | // Notify subscribers of the change 825 | dispatchEvent( 'fragmenthidden', { fragment: verticalFragments[ verticalFragments.length - 1 ] } ); 826 | return true; 827 | } 828 | } 829 | // Horizontal slides: 830 | else { 831 | var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' ); 832 | if( horizontalFragments.length ) { 833 | horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' ); 834 | 835 | // Notify subscribers of the change 836 | dispatchEvent( 'fragmenthidden', { fragment: horizontalFragments[ horizontalFragments.length - 1 ] } ); 837 | return true; 838 | } 839 | } 840 | 841 | return false; 842 | } 843 | 844 | /** 845 | * Triggers a navigation to the specified indices. 846 | * 847 | * @param {Number} h The horizontal index of the slide to show 848 | * @param {Number} v The vertical index of the slide to show 849 | */ 850 | function navigateTo( h, v ) { 851 | slide( h, v ); 852 | } 853 | 854 | function navigateLeft() { 855 | // Prioritize hiding fragments 856 | if( overviewIsActive() || previousFragment() === false ) { 857 | slide( indexh - 1, 0 ); 858 | } 859 | } 860 | function navigateRight() { 861 | // Prioritize revealing fragments 862 | if( overviewIsActive() || nextFragment() === false ) { 863 | slide( indexh + 1, 0 ); 864 | } 865 | } 866 | function navigateUp() { 867 | // Prioritize hiding fragments 868 | if( overviewIsActive() || previousFragment() === false ) { 869 | slide( indexh, indexv - 1 ); 870 | } 871 | } 872 | function navigateDown() { 873 | // Prioritize revealing fragments 874 | if( overviewIsActive() || nextFragment() === false ) { 875 | slide( indexh, indexv + 1 ); 876 | } 877 | } 878 | 879 | /** 880 | * Navigates backwards, prioritized in the following order: 881 | * 1) Previous fragment 882 | * 2) Previous vertical slide 883 | * 3) Previous horizontal slide 884 | */ 885 | function navigatePrev() { 886 | // Prioritize revealing fragments 887 | if( previousFragment() === false ) { 888 | if( availableRoutes().up ) { 889 | navigateUp(); 890 | } 891 | else { 892 | // Fetch the previous horizontal slide, if there is one 893 | var previousSlide = document.querySelector( '.reveal .slides>section.past:nth-child(' + indexh + ')' ); 894 | 895 | if( previousSlide ) { 896 | indexv = ( previousSlide.querySelectorAll('section').length + 1 ) || 0; 897 | indexh --; 898 | slide(); 899 | } 900 | } 901 | } 902 | } 903 | 904 | /** 905 | * Same as #navigatePrev() but navigates forwards. 906 | */ 907 | function navigateNext() { 908 | // Prioritize revealing fragments 909 | if( nextFragment() === false ) { 910 | availableRoutes().down ? navigateDown() : navigateRight(); 911 | } 912 | } 913 | 914 | /** 915 | * Toggles the slide overview mode on and off. 916 | */ 917 | function toggleOverview() { 918 | if( overviewIsActive() ) { 919 | deactivateOverview(); 920 | } 921 | else { 922 | activateOverview(); 923 | } 924 | } 925 | 926 | // Expose some methods publicly 927 | return { 928 | initialize: initialize, 929 | navigateTo: navigateTo, 930 | navigateLeft: navigateLeft, 931 | navigateRight: navigateRight, 932 | navigateUp: navigateUp, 933 | navigateDown: navigateDown, 934 | navigatePrev: navigatePrev, 935 | navigateNext: navigateNext, 936 | toggleOverview: toggleOverview, 937 | 938 | addEventListeners: addEventListeners, 939 | removeEventListeners: removeEventListeners, 940 | 941 | // Forward event binding to the reveal DOM element 942 | addEventListener: function( type, listener, useCapture ) { 943 | ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture ); 944 | }, 945 | removeEventListener: function( type, listener, useCapture ) { 946 | ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture ); 947 | } 948 | }; 949 | 950 | })(); 951 | 952 | -------------------------------------------------------------------------------- /js/reveal.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * reveal.js 1.4 3 | * http://lab.hakim.se/reveal-js 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | var Reveal=(function(){var j=".reveal .slides>section",a=".reveal .slides>section.present>section",e=!!("ontouchstart" in window),k=0,c=0,H={controls:true,progress:false,history:false,loop:false,mouseWheel:true,rollingLinks:true,transition:"default",theme:"default"},T=[],d={},J=document.body.style.WebkitPerspective!==undefined||document.body.style.MozPerspective!==undefined||document.body.style.msPerspective!==undefined||document.body.style.OPerspective!==undefined||document.body.style.perspective!==undefined,l=document.body.style.WebkitTransform!==undefined||document.body.style.MozTransform!==undefined||document.body.style.msTransform!==undefined||document.body.style.OTransform!==undefined||document.body.style.transform!==undefined,x=!!document.body.classList; 9 | mouseWheelTimeout=0,writeURLTimeout=0,touch={startX:0,startY:0,startSpan:0,startCount:0,handled:false,threshold:40};function h(V){if((!l&&!J)||!x){document.body.setAttribute("class","no-transforms"); 10 | return;}d.wrapper=document.querySelector(".reveal");d.progress=document.querySelector(".reveal .progress");d.progressbar=document.querySelector(".reveal .progress span"); 11 | if(H.controls){d.controls=document.querySelector(".reveal .controls");d.controlsLeft=document.querySelector(".reveal .controls .left");d.controlsRight=document.querySelector(".reveal .controls .right"); 12 | d.controlsUp=document.querySelector(".reveal .controls .up");d.controlsDown=document.querySelector(".reveal .controls .down");}z();q(H,V);C();D();if(navigator.userAgent.match(/(iphone|ipod|android)/i)){document.documentElement.style.overflow="scroll"; 13 | document.body.style.height="120%";window.addEventListener("load",P,false);window.addEventListener("orientationchange",P,false);}}function C(){if(J===false){H.transition="linear"; 14 | }if(H.controls&&d.controls){d.controls.style.display="block";}if(H.progress){d.progress.style.display="block";}if(H.transition!=="default"){d.wrapper.classList.add(H.transition); 15 | }if(H.theme!=="default"){d.wrapper.classList.add(H.theme);}if(H.mouseWheel){document.addEventListener("DOMMouseScroll",m,false);document.addEventListener("mousewheel",m,false); 16 | }if(H.rollingLinks){E();}}function z(){document.addEventListener("keydown",S,false);document.addEventListener("touchstart",v,false);document.addEventListener("touchmove",R,false); 17 | document.addEventListener("touchend",L,false);window.addEventListener("hashchange",t,false);if(H.controls&&d.controls){d.controlsLeft.addEventListener("click",n(w),false); 18 | d.controlsRight.addEventListener("click",n(i),false);d.controlsUp.addEventListener("click",n(r),false);d.controlsDown.addEventListener("click",n(A),false); 19 | }}function K(){document.removeEventListener("keydown",S,false);document.removeEventListener("touchstart",v,false);document.removeEventListener("touchmove",R,false); 20 | document.removeEventListener("touchend",L,false);window.removeEventListener("hashchange",t,false);if(H.controls&&d.controls){d.controlsLeft.removeEventListener("click",n(w),false); 21 | d.controlsRight.removeEventListener("click",n(i),false);d.controlsUp.removeEventListener("click",n(r),false);d.controlsDown.removeEventListener("click",n(A),false); 22 | }}function q(W,V){for(var X in V){W[X]=V[X];}}function I(X,V){var Y=X.x-V.x,W=X.y-V.y;return Math.sqrt(Y*Y+W*W);}function n(V){return function(W){W.preventDefault(); 23 | V.call();};}function P(){setTimeout(function(){window.scrollTo(0,1);},0);}function S(W){if(W.target.contentEditable!="inherit"||W.shiftKey||W.altKey||W.ctrlKey||W.metaKey){return; 24 | }var V=false;switch(W.keyCode){case 80:case 33:N();V=true;break;case 78:case 34:u();V=true;break;case 72:case 37:w();V=true;break;case 76:case 39:i();V=true; 25 | break;case 75:case 38:r();V=true;break;case 74:case 40:A();V=true;break;case 36:F(0);V=true;break;case 35:F(Number.MAX_VALUE);V=true;break;case 32:O()?Q():u(); 26 | V=true;break;case 13:if(O()){Q();V=true;}break;}if(V){W.preventDefault();}else{if(W.keyCode===27&&J){if(O()){Q();}else{B();}W.preventDefault();}}}function v(V){touch.startX=V.touches[0].clientX; 27 | touch.startY=V.touches[0].clientY;touch.startCount=V.touches.length;if(V.touches.length===2){touch.startSpan=I({x:V.touches[1].clientX,y:V.touches[1].clientY},{x:touch.startX,y:touch.startY}); 28 | }}function R(aa){if(!touch.handled){var Y=aa.touches[0].clientX;var X=aa.touches[0].clientY;if(aa.touches.length===2&&touch.startCount===2){var Z=I({x:aa.touches[1].clientX,y:aa.touches[1].clientY},{x:touch.startX,y:touch.startY}); 29 | if(Math.abs(touch.startSpan-Z)>touch.threshold){touch.handled=true;if(Ztouch.threshold&&Math.abs(W)>Math.abs(V)){touch.handled=true;w();}else{if(W<-touch.threshold&&Math.abs(W)>Math.abs(V)){touch.handled=true;i();}else{if(V>touch.threshold){touch.handled=true; 31 | r();}else{if(V<-touch.threshold){touch.handled=true;A();}}}}}}aa.preventDefault();}}function L(V){touch.handled=false;}function m(V){clearTimeout(mouseWheelTimeout); 32 | mouseWheelTimeout=setTimeout(function(){var W=V.detail||-V.wheelDelta;if(W>0){u();}else{N();}},100);}function t(V){D();}function E(){if(J){var W=document.querySelectorAll(".reveal .slides section a:not(.image)"); 33 | for(var X=0,V=W.length;X'+Y.innerHTML+"";}}}}function B(){d.wrapper.classList.add("overview");var V=Array.prototype.slice.call(document.querySelectorAll(j)); 35 | for(var aa=0,Y=V.length;aa3?"none":"block"; 44 | }aa[Z].classList.remove("past");aa[Z].classList.remove("present");aa[Z].classList.remove("future");if(ZY){aa[Z].classList.add("future"); 45 | }}if(V.querySelector("section")){aa[Z].classList.add("stack");}}aa[Y].classList.add("present");var X=aa[Y].getAttribute("data-state");if(X){T=T.concat(X.split(" ")); 46 | }}else{Y=0;}return Y;}function b(ab,ag){var Y=T.concat();T.length=0;var af=k,W=c;k=U(j,ab===undefined?k:ab);c=U(a,ag===undefined?c:ag);stateLoop:for(var Z=0,ad=T.length; 47 | Z0,right:k0,down:c0||c>0){V+=k;}if(c>0){V+="/"+c;}window.location.hash=V;}}function o(W,V){var X=document.createEvent("HTMLEvents",1,2); 55 | X.initEvent(W,true,true);q(X,V);d.wrapper.dispatchEvent(X);}function s(){if(document.querySelector(a+".present")){var W=document.querySelectorAll(a+".present .fragment:not(.visible)"); 56 | if(W.length){W[0].classList.add("visible");o("fragmentshown",{fragment:W[0]});return true;}}else{var V=document.querySelectorAll(j+".present .fragment:not(.visible)"); 57 | if(V.length){V[0].classList.add("visible");o("fragmentshown",{fragment:V[0]});return true;}}return false;}function G(){if(document.querySelector(a+".present")){var W=document.querySelectorAll(a+".present .fragment.visible"); 58 | if(W.length){W[W.length-1].classList.remove("visible");o("fragmenthidden",{fragment:W[0]});return true;}}else{var V=document.querySelectorAll(j+".present .fragment.visible"); 59 | if(V.length){V[V.length-1].classList.remove("visible");o("fragmenthidden",{fragment:V[0]});return true;}}return false;}function F(W,V){b(W,V);}function w(){if(O()||G()===false){b(k-1,0); 60 | }}function i(){if(O()||s()===false){b(k+1,0);}}function r(){if(O()||G()===false){b(k,c-1);}}function A(){if(O()||s()===false){b(k,c+1);}}function N(){if(G()===false){if(f().up){r(); 61 | }else{var V=document.querySelector(".reveal .slides>section.past:nth-child("+k+")");if(V){c=(V.querySelectorAll("section").length+1)||0;k--;b();}}}}function u(){if(s()===false){f().down?A():i(); 62 | }}function M(){if(O()){Q();}else{B();}}return{initialize:h,navigateTo:F,navigateLeft:w,navigateRight:i,navigateUp:r,navigateDown:A,navigatePrev:N,navigateNext:u,toggleOverview:M,addEventListeners:z,removeEventListeners:K,addEventListener:function(W,X,V){(d.wrapper||document.querySelector(".reveal")).addEventListener(W,X,V); 63 | },removeEventListener:function(W,X,V){(d.wrapper||document.querySelector(".reveal")).removeEventListener(W,X,V);}};})(); -------------------------------------------------------------------------------- /lib/classList.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ 2 | if(typeof document!=="undefined"&&!("classList" in document.createElement("a"))){(function(j){var a="classList",f="prototype",m=(j.HTMLElement||j.Element)[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | color: #000; 10 | background: #f8f8ff 11 | } 12 | 13 | pre .comment, 14 | pre .template_comment, 15 | pre .diff .header, 16 | pre .javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | pre .keyword, 22 | pre .css .rule .keyword, 23 | pre .winutils, 24 | pre .javascript .title, 25 | pre .lisp .title, 26 | pre .nginx .title, 27 | pre .subst, 28 | pre .request, 29 | pre .status { 30 | color: #000; 31 | font-weight: bold 32 | } 33 | 34 | pre .number, 35 | pre .hexcolor { 36 | color: #40a070 37 | } 38 | 39 | pre .string, 40 | pre .tag .value, 41 | pre .phpdoc, 42 | pre .tex .formula { 43 | color: #d14 44 | } 45 | 46 | pre .title, 47 | pre .id { 48 | color: #900; 49 | font-weight: bold 50 | } 51 | 52 | pre .javascript .title, 53 | pre .lisp .title, 54 | pre .subst { 55 | font-weight: normal 56 | } 57 | 58 | pre .class .title, 59 | pre .haskell .type, 60 | pre .vhdl .literal, 61 | pre .tex .command { 62 | color: #458; 63 | font-weight: bold 64 | } 65 | 66 | pre .tag, 67 | pre .tag .title, 68 | pre .rules .property, 69 | pre .django .tag .keyword { 70 | color: #000080; 71 | font-weight: normal 72 | } 73 | 74 | pre .attribute, 75 | pre .variable, 76 | pre .instancevar, 77 | pre .lisp .body { 78 | color: #008080 79 | } 80 | 81 | pre .regexp { 82 | color: #009926 83 | } 84 | 85 | pre .class { 86 | color: #458; 87 | font-weight: bold 88 | } 89 | 90 | pre .symbol, 91 | pre .ruby .symbol .string, 92 | pre .ruby .symbol .keyword, 93 | pre .ruby .symbol .keymethods, 94 | pre .lisp .keyword, 95 | pre .tex .special, 96 | pre .input_number { 97 | color: #990073 98 | } 99 | 100 | pre .builtin, 101 | pre .built_in, 102 | pre .lisp .title { 103 | color: #0086b3 104 | } 105 | 106 | pre .preprocessor, 107 | pre .pi, 108 | pre .doctype, 109 | pre .shebang, 110 | pre .cdata { 111 | color: #999; 112 | font-weight: bold 113 | } 114 | 115 | pre .deletion { 116 | background: #fdd 117 | } 118 | 119 | pre .addition { 120 | background: #dfd 121 | } 122 | 123 | pre .diff .change { 124 | background: #0086b3 125 | } 126 | 127 | pre .chunk { 128 | color: #aaa 129 | } 130 | 131 | pre .tex .formula { 132 | opacity: 0.5; 133 | } 134 | -------------------------------------------------------------------------------- /lib/prism.css: -------------------------------------------------------------------------------- 1 | /** 2 | * okaidia theme for JavaScript, CSS and HTML 3 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 4 | * @author ocodia 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | color: #f8f8f2; 10 | text-shadow: 0 1px rgba(0,0,0,0.3); 11 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | 17 | -moz-tab-size: 4; 18 | -o-tab-size: 4; 19 | tab-size: 4; 20 | 21 | -webkit-hyphens: none; 22 | -moz-hyphens: none; 23 | -ms-hyphens: none; 24 | hyphens: none; 25 | } 26 | 27 | /* Code blocks */ 28 | pre[class*="language-"] { 29 | padding: 1em; 30 | margin: .5em 0; 31 | overflow: auto; 32 | border-radius: 0.3em; 33 | } 34 | 35 | :not(pre) > code[class*="language-"], 36 | pre[class*="language-"] { 37 | background: #272822; 38 | } 39 | 40 | /* Inline code */ 41 | :not(pre) > code[class*="language-"] { 42 | padding: .1em; 43 | border-radius: .3em; 44 | } 45 | 46 | .token.comment, 47 | .token.prolog, 48 | .token.doctype, 49 | .token.cdata { 50 | color: slategray; 51 | } 52 | 53 | .token.punctuation { 54 | color: #f8f8f2; 55 | } 56 | 57 | .namespace { 58 | opacity: .7; 59 | } 60 | 61 | .token.property, 62 | .token.tag { 63 | color: #f92672; 64 | } 65 | 66 | .token.boolean, 67 | .token.number{ 68 | color: #ae81ff; 69 | } 70 | 71 | .token.selector, 72 | .token.attr-name, 73 | .token.string { 74 | color: #a6e22e; 75 | } 76 | 77 | 78 | .token.operator, 79 | .token.entity, 80 | .token.url, 81 | .language-css .token.string, 82 | .style .token.string { 83 | color: #f8f8f2; 84 | } 85 | 86 | .token.atrule, 87 | .token.attr-value 88 | { 89 | color: #e6db74; 90 | } 91 | 92 | 93 | .token.keyword{ 94 | color: #66d9ef; 95 | } 96 | 97 | .token.regex, 98 | .token.important { 99 | color: #fd971f; 100 | } 101 | 102 | .token.important { 103 | font-weight: bold; 104 | } 105 | 106 | .token.entity { 107 | cursor: help; 108 | } 109 | -------------------------------------------------------------------------------- /lib/prism.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(/e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; 6 | Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; 7 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//g,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*{))/gi,inside:{punctuation:/[;:]/g}},url:/url\((["']?).*?\1\)/gi,selector:/[^\{\}\s][^\{\};]*(?=\s*\{)/g,property:/(\b|\B)[\w-]+(?=\s*:)/ig,string:/("|')(\\?.)*?\1/g,important:/\B!important\b/gi,ignore:/&(lt|gt|amp);/gi,punctuation:/[\{\};:]/g};Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{style:{pattern:/(<|<)style[\w\W]*?(>|>)[\w\W]*?(<|<)\/style(>|>)/ig,inside:{tag:{pattern:/(<|<)style[\w\W]*?(>|>)|(<|<)\/style(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css}}});; 8 | Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; 9 | ; 10 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}}); 11 | ; 12 | Prism.languages.php=Prism.languages.extend("clike",{keyword:/\b(and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|extends|private|protected|parent|static|throw|null|echo|print|trait|namespace|use|final|yield|goto|instanceof|finally|try|catch)\b/ig, constant:/\b[A-Z0-9_]{2,}\b/g});Prism.languages.insertBefore("php","keyword",{delimiter:/(\?>|<\?php|<\?)/ig,variable:/(\$\w+)\b/ig,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/g,lookbehind:!0,inside:{punctuation:/\\/}}});Prism.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/g,lookbehind:!0}}); Prism.languages.markup&&(Prism.hooks.add("before-highlight",function(a){"php"===a.language&&(a.tokenStack=[],a.code=a.code.replace(/(?:<\?php|<\?|<\?php|<\?)[\w\W]*?(?:\?>|\?>)/ig,function(b){a.tokenStack.push(b);return"{{{PHP"+a.tokenStack.length+"}}}"}))}),Prism.hooks.add("after-highlight",function(a){if("php"===a.language){for(var b=0,c;c=a.tokenStack[b];b++)a.highlightedCode=a.highlightedCode.replace("{{{PHP"+(b+1)+"}}}",Prism.highlight(c,a.grammar,"php"));a.element.innerHTML=a.highlightedCode}}), Prism.hooks.add("wrap",function(a){"php"===a.language&&"markup"===a.type&&(a.content=a.content.replace(/(\{\{\{PHP[0-9]+\}\}\})/g,'$1'))}),Prism.languages.insertBefore("php","comment",{markup:{pattern:/(<|<)[^?]\/?(.*?)(>|>)/g,inside:Prism.languages.markup},php:/\{\{\{PHP[0-9]+\}\}\}/g}));; 13 | Prism.languages.scss=Prism.languages.extend("css",{comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|\/\/.*?(\r?\n|$))/g,lookbehind:!0},atrule:/@[\w-]+(?=\s+(\(|\{|;))/gi,url:/([-a-z]+-)*url(?=\()/gi,selector:/([^@;\{\}\(\)]?([^@;\{\}\(\)]|&|\#\{\$[-_\w]+\})+)(?=\s*\{(\}|\s|[^\}]+(:|\{)[^\}]+))/gm});Prism.languages.insertBefore("scss","atrule",{keyword:/@(if|else if|else|for|each|while|import|extend|debug|warn|mixin|include|function|return)|(?=@for\s+\$[-_\w]+\s)+from/i});Prism.languages.insertBefore("scss","property",{variable:/((\$[-_\w]+)|(#\{\$[-_\w]+\}))/i});Prism.languages.insertBefore("scss","ignore",{placeholder:/%[-_\w]+/i,statement:/\B!(default|optional)\b/gi,"boolean":/\b(true|false)\b/g,"null":/\b(null)\b/g,operator:/\s+([-+]{1,2}|={1,2}|!=|\|?\||\?|\*|\/|\%)\s+/g}); 14 | ; 15 | Prism.languages.bash=Prism.languages.extend("clike",{comment:{pattern:/(^|[^"{\\])(#.*?(\r?\n|$))/g,lookbehind:!0},string:{pattern:/("|')(\\?[\s\S])*?\1/g,inside:{property:/\$([a-zA-Z0-9_#\?\-\*!@]+|\{[^\}]+\})/g}},keyword:/\b(if|then|else|elif|fi|for|break|continue|while|in|case|function|select|do|done|until|echo|exit|return|set|declare)\b/g});Prism.languages.insertBefore("bash","keyword",{property:/\$([a-zA-Z0-9_#\?\-\*!@]+|\{[^}]+\})/g});Prism.languages.insertBefore("bash","comment",{important:/(^#!\s*\/bin\/bash)|(^#!\s*\/bin\/sh)/g}); 16 | ; 17 | -------------------------------------------------------------------------------- /lib/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | pre code { 9 | display: block; padding: 0.5em; 10 | background: #3F3F3F; 11 | color: #DCDCDC; 12 | } 13 | 14 | pre .keyword, 15 | pre .tag, 16 | pre .django .tag, 17 | pre .django .keyword, 18 | pre .css .class, 19 | pre .css .id, 20 | pre .lisp .title { 21 | color: #E3CEAB; 22 | } 23 | 24 | pre .django .template_tag, 25 | pre .django .variable, 26 | pre .django .filter .argument { 27 | color: #DCDCDC; 28 | } 29 | 30 | pre .number, 31 | pre .date { 32 | color: #8CD0D3; 33 | } 34 | 35 | pre .dos .envvar, 36 | pre .dos .stream, 37 | pre .variable, 38 | pre .apache .sqbracket { 39 | color: #EFDCBC; 40 | } 41 | 42 | pre .dos .flow, 43 | pre .diff .change, 44 | pre .python .exception, 45 | pre .python .built_in, 46 | pre .literal, 47 | pre .tex .special { 48 | color: #EFEFAF; 49 | } 50 | 51 | pre .diff .chunk, 52 | pre .ruby .subst { 53 | color: #8F8F8F; 54 | } 55 | 56 | pre .dos .keyword, 57 | pre .python .decorator, 58 | pre .class .title, 59 | pre .haskell .label, 60 | pre .function .title, 61 | pre .ini .title, 62 | pre .diff .header, 63 | pre .ruby .class .parent, 64 | pre .apache .tag, 65 | pre .nginx .built_in, 66 | pre .tex .command, 67 | pre .input_number { 68 | color: #efef8f; 69 | } 70 | 71 | pre .dos .winutils, 72 | pre .ruby .symbol, 73 | pre .ruby .symbol .string, 74 | pre .ruby .symbol .keyword, 75 | pre .ruby .symbol .keymethods, 76 | pre .ruby .string, 77 | pre .ruby .instancevar { 78 | color: #DCA3A3; 79 | } 80 | 81 | pre .diff .deletion, 82 | pre .string, 83 | pre .tag .value, 84 | pre .preprocessor, 85 | pre .built_in, 86 | pre .sql .aggregate, 87 | pre .javadoc, 88 | pre .smalltalk .class, 89 | pre .smalltalk .localvars, 90 | pre .smalltalk .array, 91 | pre .css .rules .value, 92 | pre .attr_selector, 93 | pre .pseudo, 94 | pre .apache .cbracket, 95 | pre .tex .formula { 96 | color: #CC9393; 97 | } 98 | 99 | pre .shebang, 100 | pre .diff .addition, 101 | pre .comment, 102 | pre .java .annotation, 103 | pre .template_comment, 104 | pre .pi, 105 | pre .doctype { 106 | color: #7F9F7F; 107 | } 108 | 109 | pre .xml .css, 110 | pre .xml .javascript, 111 | pre .xml .vbscript, 112 | pre .tex .formula { 113 | opacity: 0.5; 114 | } 115 | 116 | --------------------------------------------------------------------------------