webshot/0000755000176200001440000000000014446417572011743 5ustar liggesuserswebshot/NAMESPACE0000644000176200001440000000046113565533764013166 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(appshot,character) S3method(appshot,shiny.appobj) S3method(print,webshot) export("%>%") export(appshot) export(install_phantomjs) export(is_phantomjs_installed) export(resize) export(rmdshot) export(shrink) export(webshot) importFrom(magrittr,"%>%") webshot/README.md0000644000176200001440000001347514446405775013236 0ustar liggesusers # webshot [![R-CMD-check](https://github.com/wch/webshot/workflows/R-CMD-check/badge.svg)](https://github.com/wch/webshot/actions) **Webshot** makes it easy to take screenshots of web pages from R. It can also: - Run Shiny applications locally and take screenshots of the application. - Render R Markdown documents and take screenshots of the document. Webshot can handle both static Rmd documents and interactive ones (those with `runtime: shiny`). See the [introduction article](https://wch.github.io/webshot/articles/intro.html) for examples in addition to the ones below. ## Installation Webshot can be installed from CRAN. Webshot also requires the external program [PhantomJS](https://phantomjs.org/). You may either download PhantomJS from its website, or use the function `webshot::install_phantomjs()` to install it automatically. ``` r install.packages("webshot") webshot::install_phantomjs() ``` ## Usage By default, `webshot` will use a 992x744 pixel viewport (a virtual browser window) and take a screenshot of the entire page, even the portion outside the viewport. ``` r library(webshot) webshot("https://www.r-project.org/", "r.png") webshot("https://www.r-project.org/", "r.pdf") # Can also output to PDF ``` You can clip it to just the viewport region: ``` r webshot("https://www.r-project.org/", "r-viewport.png", cliprect = "viewport") ``` You can also get screenshots of a portion of a web page using CSS selectors. If there are multiple matches for the CSS selector, it will use the first match. ``` r webshot("https://www.r-project.org/", "r-sidebar.png", selector = ".sidebar") ``` If you supply multiple CSS selectors, it will take a screenshot containing all of the selected items. ``` r webshot("https://www.r-project.org/", "r-selectors.png", selector = c("#getting-started", "#news")) ``` The clipping rectangle can be expanded to capture some area outside the selected items: ``` r webshot("https://www.r-project.org/", "r-expand.png", selector = "#getting-started", expand = c(40, 20, 40, 20)) ``` You can take higher-resolution screenshots with the `zoom` option. This isn’t exactly the same as taking a screenshot with a HiDPI (“Retina”) device: it is like increasing the zoom to 200% in a desktop browser and doubling the height and width of the browser window. This differs from using a HiDPI device because some web pages load different, higher-resolution images when they know they will be displayed on a HiDPI device (but using zoom will not report that there is a HiDPI device). ``` r webshot("https://www.r-project.org/", "r-sidebar-zoom.png", selector = ".sidebar", zoom = 2) ``` ### Vectorization All parameters of function `webshot`. That means that multiple screenshots can be taken with a single command. When taking a lot of screenshots, vectorization can divide by 5 the execution time. ``` r # Take a screenshot of different sites webshot(c("https://www.r-project.org/", "https://github.com/wch/webshot"), file = c("r.png", "webshot.png")) # Save screenshots of the same site in different formats webshot("https://www.r-project.org/", file = c("r.png", "r.pdf")) # Take screenshots of different sections of the same site. # Note that unlike arguments "url" and "file", a list is required to specify # multiple selectors. This is also the case for arguments "cliprect" and # "expand" webshot("http://rstudio.github.io/leaflet/", file = c("leaflet_features.png", "leaflet_install.png"), selector = list("#features", "#installation")) ``` ### Screenshots of Shiny applications The `appshot()` function will run a Shiny app locally in a separate R process, and take a screenshot of it. After taking the screenshot, it will kill the R process that is running the Shiny app. ``` r # Get the directory of one of the Shiny examples appdir <- system.file("examples", "01_hello", package="shiny") appshot(appdir, "01_hello.png") ``` ### Screenshots of R Markdown documents The `rmdshot()` function takes screenshots of R Markdown documents. For static R Markdown documents, it renders them to HTML in a temporary directory (using `rmarkdown::render()`)and then takes a screenshot. For dynamic R Markdown documents, it runs them using `rmarkdown::run()` in a separate R process and then takes a screenshot. After taking the screenshot, it will kill the R process that is running the document. ``` r rmdshot("document.rmd", "document.png") ``` ### Manipulating images If you have GraphicsMagick (recommended) or ImageMagick installed, you can pass the result to `resize()` to resize the image after taking the screenshot. This can take any valid ImageMagick geometry specifictaion, like `"75%"`, or `"400x"` (for an image 400 pixels wide). However, you may get different (and often better) results by using the `zoom` option: the fonts and graphical elements will render more sharply. However, compared to simply resizing, zooming out may result in slightly different positioning of text and layout elements. You can also call `shrink()`, which runs [OptiPNG](https://optipng.sourceforge.net/) to shrink the PNG file losslessly. ``` r webshot("https://www.r-project.org/", "r-small-resized.png") %>% resize("75%") %>% shrink() # Using zoom instead of resize() webshot("https://www.r-project.org/", "r-small-zoomed.png", zoom = 0.75) %>% shrink() # Can specify pixel dimensions for resize() webshot("https://www.r-project.org/", "r-small.png") %>% resize("400x") %>% shrink() ``` To illustrate the difference between `resize()` and `zoom`, here is an image with `resize("50%")`: ![](man/figures/r-small-resized.png) And here is one with `zoom = 0.5`. If you look closely, you’ll see that the text and graphics are sharper. You’ll also see that the bullet points and text are positioned slightly differently: ![](man/figures/r-small-zoomed.png) webshot/man/0000755000176200001440000000000014225315440012500 5ustar liggesuserswebshot/man/install_phantomjs.Rd0000644000176200001440000000434214225315117016524 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{install_phantomjs} \alias{install_phantomjs} \title{Install PhantomJS} \usage{ install_phantomjs( version = "2.1.1", baseURL = "https://github.com/wch/webshot/releases/download/v0.3.1/", force = FALSE ) } \arguments{ \item{version}{The version number of PhantomJS.} \item{baseURL}{The base URL for the location of PhantomJS binaries for download. If the default download site is unavailable, you may specify an alternative mirror, such as \code{"https://bitbucket.org/ariya/phantomjs/downloads/"}.} \item{force}{Install PhantomJS even if the version installed is the latest or if the requested version is older. This is useful to reinstall or downgrade the version of PhantomJS.} } \value{ \code{NULL} (the executable is written to a system directory). } \description{ Download the zip package, unzip it, and copy the executable to a system directory in which \pkg{webshot} can look for the PhantomJS executable. } \details{ This function was designed primarily to help Windows users since it is cumbersome to modify the \code{PATH} variable. Mac OS X users may install PhantomJS via Homebrew. If you download the package from the PhantomJS website instead, please make sure the executable can be found via the \code{PATH} variable. On Windows, the directory specified by the environment variable \code{APPDATA} is used to store \file{phantomjs.exe}. On OS X, the directory \file{~/Library/Application Support} is used. On other platforms (such as Linux), the directory \file{~/bin} is used. If these directories are not writable, the directory \file{PhantomJS} under the installation directory of the \pkg{webshot} package will be tried. If this directory still fails, you will have to install PhantomJS by yourself. If PhantomJS is not already installed on the computer, this function will attempt to install it. However, if the version of PhantomJS installed is greater than or equal to the requested version, this function will not perform the installation procedure again unless the \code{force} parameter is set to \code{TRUE}. As a result, this function may also be used to reinstall or downgrade the version of PhantomJS found. } webshot/man/rmdshot.Rd0000644000176200001440000000362214225315117014453 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/rmdshot.R \name{rmdshot} \alias{rmdshot} \title{Take a snapshot of an R Markdown document} \usage{ rmdshot( doc, file = "webshot.png", ..., delay = NULL, rmd_args = list(), port = getOption("shiny.port"), envvars = NULL ) } \arguments{ \item{doc}{The path to a Rmd document.} \item{file}{A vector of names of output files. Should end with \code{.png}, \code{.pdf}, or \code{.jpeg}. If several screenshots have to be taken and only one filename is provided, then the function appends the index number of the screenshot to the file name.} \item{...}{Other arguments to pass on to \code{\link{webshot}}.} \item{delay}{Time to wait before taking screenshot, in seconds. Sometimes a longer delay is needed for all assets to display properly. If NULL (the default), then it will use 0.2 seconds for static Rmd documents, and 3 seconds for Rmd documents with runtime:shiny.} \item{rmd_args}{A list of additional arguments to pass to either \code{\link[rmarkdown]{render}} (for static Rmd documents) or \code{\link[rmarkdown]{run}} (for Rmd documents with runtime:shiny).} \item{port}{Port that Shiny will listen on.} \item{envvars}{A named character vector or named list of environment variables and values to set for the Shiny app's R process. These will be unset after the process exits. This can be used to pass configuration information to a Shiny app.} } \description{ This function can handle both static Rmd documents and Rmd documents with \code{runtime: shiny}. } \examples{ if (interactive()) { # rmdshot("rmarkdown_file.Rmd", "snapshot.png") # R Markdown file input_file <- system.file("examples/knitr-minimal.Rmd", package = "knitr") rmdshot(input_file, "minimal_rmd.png") # Shiny R Markdown file input_file <- system.file("examples/shiny.Rmd", package = "webshot") rmdshot(input_file, "shiny_rmd.png", delay = 5) } } webshot/man/webshot.Rd0000644000176200001440000001403314225315440014443 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/webshot.R \name{webshot} \alias{webshot} \title{Take a screenshot of a URL} \usage{ webshot( url = NULL, file = "webshot.png", vwidth = 992, vheight = 744, cliprect = NULL, selector = NULL, expand = NULL, delay = 0.2, zoom = 1, eval = NULL, debug = FALSE, useragent = NULL ) } \arguments{ \item{url}{A vector of URLs to visit.} \item{file}{A vector of names of output files. Should end with \code{.png}, \code{.pdf}, or \code{.jpeg}. If several screenshots have to be taken and only one filename is provided, then the function appends the index number of the screenshot to the file name.} \item{vwidth}{Viewport width. This is the width of the browser "window".} \item{vheight}{Viewport height This is the height of the browser "window".} \item{cliprect}{Clipping rectangle. If \code{cliprect} and \code{selector} are both unspecified, the clipping rectangle will contain the entire page. This can be the string \code{"viewport"}, in which case the clipping rectangle matches the viewport size, or it can be a four-element numeric vector specifying the top, left, width, and height. When taking screenshots of multiple URLs, this parameter can also be a list with same length as \code{url} with each element of the list being "viewport" or a four-elements numeric vector. This option is not compatible with \code{selector}.} \item{selector}{One or more CSS selectors specifying a DOM element to set the clipping rectangle to. The screenshot will contain these DOM elements. For a given selector, if it has more than one match, only the first one will be used. This option is not compatible with \code{cliprect}. When taking screenshots of multiple URLs, this parameter can also be a list with same length as \code{url} with each element of the list containing a vector of CSS selectors to use for the corresponding URL.} \item{expand}{A numeric vector specifying how many pixels to expand the clipping rectangle by. If one number, the rectangle will be expanded by that many pixels on all sides. If four numbers, they specify the top, right, bottom, and left, in that order. When taking screenshots of multiple URLs, this parameter can also be a list with same length as \code{url} with each element of the list containing a single number or four numbers to use for the corresponding URL.} \item{delay}{Time to wait before taking screenshot, in seconds. Sometimes a longer delay is needed for all assets to display properly.} \item{zoom}{A number specifying the zoom factor. A zoom factor of 2 will result in twice as many pixels vertically and horizontally. Note that using 2 is not exactly the same as taking a screenshot on a HiDPI (Retina) device: it is like increasing the zoom to 200% in a desktop browser and doubling the height and width of the browser window. This differs from using a HiDPI device because some web pages load different, higher-resolution images when they know they will be displayed on a HiDPI device (but using zoom will not report that there is a HiDPI device).} \item{eval}{An optional string with JavaScript code which will be evaluated after opening the page and waiting for \code{delay}, but before calculating the clipping region and taking the screenshot. See the Casper API for more information about what commands can be used to control the web page. NOTE: This is experimental and likely to change!} \item{debug}{Print out debugging messages from PhantomJS and CasperJS. This can help to diagnose problems.} \item{useragent}{The User-Agent header used to request the URL. Changing the User-Agent can mitigate rendering issues for some websites.} } \description{ Take a screenshot of a URL } \examples{ if (interactive()) { # Whole web page webshot("https://github.com/rstudio/shiny") # Might need a longer delay for all assets to display webshot("http://rstudio.github.io/leaflet", delay = 0.5) # One can also take screenshots of several URLs with only one command. # This is more efficient than calling 'webshot' multiple times. webshot(c("https://github.com/rstudio/shiny", "http://rstudio.github.io/leaflet"), delay = 0.5) # Clip to the viewport webshot("http://rstudio.github.io/leaflet", "leaflet-viewport.png", cliprect = "viewport") # Manual clipping rectangle webshot("http://rstudio.github.io/leaflet", "leaflet-clip.png", cliprect = c(200, 5, 400, 300)) # Using CSS selectors to pick out regions webshot("http://rstudio.github.io/leaflet", "leaflet-menu.png", selector = ".list-group") webshot("http://reddit.com/", "reddit-top.png", selector = c("input[type='text']", "#header-bottom-left")) # Expand selection region webshot("http://rstudio.github.io/leaflet", "leaflet-boxes.png", selector = "#installation", expand = c(10, 50, 0, 50)) # If multiple matches for a given selector, it uses the first match webshot("http://rstudio.github.io/leaflet", "leaflet-p.png", selector = "p") webshot("https://github.com/rstudio/shiny/", "shiny-stats.png", selector = "ul.numbers-summary") # Send commands to eval webshot("http://www.reddit.com/", "reddit-input.png", selector = c("#search", "#login_login-main"), eval = "casper.then(function() { // Check the remember me box this.click('#rem-login-main'); // Enter username and password this.sendKeys('#login_login-main input[type=\"text\"]', 'my_username'); this.sendKeys('#login_login-main input[type=\"password\"]', 'password'); // Now click in the search box. This results in a box expanding below this.click('#search input[type=\"text\"]'); // Wait 500ms this.wait(500); });" ) # Result can be piped to other commands like resize() and shrink() webshot("https://www.r-project.org/", "r-small.png") \%>\% resize("75\%") \%>\% shrink() # Requests can change the User-Agent header webshot( "https://www.rstudio.com/products/rstudio/download/", "rstudio.png", useragent = "Mozilla/5.0 (Macintosh; Intel Mac OS X)" ) # See more examples in the package vignette } } \seealso{ \code{\link{appshot}} for taking screenshots of Shiny applications. } webshot/man/shrink.Rd0000644000176200001440000000143013113313351014255 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/image.R \name{shrink} \alias{shrink} \title{Shrink file size of a PNG} \usage{ shrink(filename) } \arguments{ \item{filename}{Character vector containing the path of images to resize. Must be PNG files.} } \description{ This does not change size of the image in pixels, nor does it affect appearance -- it is lossless compression. This requires the program \code{optipng} to be installed. } \details{ If other operations like resizing are performed, shrinking should occur as the last step. Otherwise, if the resizing happens after file shrinking, it will be as if the shrinking didn't happen at all. } \examples{ if (interactive()) { webshot("https://www.r-project.org/", "r-shrink.png") \%>\% shrink() } } webshot/man/figures/0000755000176200001440000000000014225315117014145 5ustar liggesuserswebshot/man/figures/r-small-zoomed.png0000644000176200001440000006361214225315117017525 0ustar liggesusersPNG  IHDR"c pHYs  ggΜJ5}bXυ7b1Y,@gX,bX tbX,bX,:b,@gX,bX tb1Y,}tt$+'/-#;-#+#+77"`X gH*XWWwMm]N~aRJz|RjL|RtlBdL|dt\DTlXDLhxtHXdphDpHDPpX୰[aB"SJJ;`X zމZ%$'%Q1qZݣpp< (f`[7p/04,JTpVyS 9r) f۶m}+-HJLIKHJOLQnޞ׳]W{yc-ZHJuYGX2*W_}EEEo c@::s24AO6ץիNW=[pliƍċ9sJjiiyb@߹s7\]]WX!\ZZ:c}||=*=uꔁc6)QիW?ܟzY 'ƦLh.DjGDaol\R\|2#bCâ@k7tO_iq񊫣gIIӪ2M駟dϛ7 ڰaC~~gϞ1cҥK---;::&tR__/?cS?@!) Q(ޅ D;6mB4\Yk,Ez{{׭[qRP&k@ ˶?z&Ę^~.))IAEEE}^tfuuuEB?3S!e9ťem Ct=yy׮tppmlX[;Z[_/z6xp(&9E;{,8Lq"Pee پ}gİU[[;}t } B&)jjjhFCC9yiO^A}/n쿙ުi,>7ӛc t Gz{BKqq4trrGgUUe e>$Z;;;p3gD`~~Hlm]ϝ;@e/]8ͣAAA 谵)'UEb=@1N 6jjV9~G6is{m#}b5 nY|E9ziF@z[ᠹ댌g [.b'[}ƲaJUPP` ٵkc˾322/ _~ x Ç3X/aeyt\❦fģ5{e}]p'k^^^f {xZXX?oeg$A1RYYHpEtRSSS'u^o=uV 2ސȨ\!ҽQQS===k֬;pB?&Etс銀?~Wg9A=tsY`k t֋̜h%yhB4~n-p4;;47={ZXtڵ͛71k*++k@Gp$;v~/gϞg0",odd4o<8gΜ+V:u 6|,Q?|ġ9jȱT]]{nL@,,,`W_?lIz!6/htlQ{@ONf=yI;[a5wiVziuH&33˳g,\ydooO@Υ,VBr^G$8.Љ_|}tcցGpoEwI55x{nБ׋???nk =-#[͵󴤤f]DZ"> @=)~ne @~Out%ϟ?_'@WPtg6Έ@W\t߯]9=0E=,4g3gܽ{wEE755^SWK|zxz_O%M>4Zz>:4[QQuSubV,q]k7/]n_hYq))FƷMhՂ3iwWiW3ĩbb@ssCC5hgZz6=N]qSD3)*B"{{ ]GPFݗ呖iKK;'Sk<|\reժU|͌3ϟzӧOGFF" $EFXWBRR%KИ8pҥ'O4lɷ3yv! )##q^6e{aDs|﹚Rd.gΜ+W 򣗼 :k,DXd ,2\}ʒ'v-*#ǹs%''2\Q<ח9,ḌG 曭[hTv ogĿmg hmAy(ePx|JHII,ktTt+>Ьz?Kn+U?ʶ%u_hm45mTY.++%1}ьKɸY:7n݇͛7'~_ .āNf8-d\8qmllt.f(#-^X_|!_5-,./AhѢ>UKKȏ2 tcJ+  "ۜh}뺻5---mjǣ'{b?hl4׎|iUO6W0?}<)1IɑyfxJ`s1&>??;wS0QҪt*@SǷ"]eI!rp%bmkÅ S?nݺ%fELاa剂QGDsuu-/f/֙;}4􆆆ݻw)0@V}R?ggrٳFdJGILLDLQ/:TN~#P׍\=>CFT[$P??k?~~s'6nN355zwwZVx7 tV,Uzm *@JԥH2)%2;Ν;'+5US(m T\&]̳(;ɗjg |ӫWQL\*e3ПzzzRaGsWoNNW9:Ҳ+{`˷tZ.K}q rhD>(8%noxׂRq{/_.muGa BRA-ۦ }mq^[:xN Lw'Zk gj4UN780KecŸ. 8PV*؇{a5I0` Hy3g&&&0`"a6|<'%%0 9͞=<]zef2JX4Ov.믿FӧOEDDsE8J*"_l[n#0'OѶmн):v e(| M!RF$Y1յ@םoK٧gD稪醂֮A\Xգ1) 2ПTΞH~=>KXL\XXfaaTQQqUad?;@O^}4M\Qk|Ű3f?[~ks6>5A/ @gC/D*h?j6k'bT1hX>LϘ1cѢE9'g=@gX,:b,bX,bX,:b1Y,@gX,֋t܆־|RjJ&xlUK^l>,o@P- @wH2.oe7v9V$h&17 *sbe fm{@|O>A>+VթKNC$('b?W{W>>z9c E`}}~/]$B? 7x_/ df͚'})ʩvJj*ecgKUZZx6nܸ|rlxx@ ]t$22Y=ǖ-[(… ?#G>cJk믿+ra-xCǃ]8i.Zb1ЧA{@1v+poOW(<<6&[[[bBe{o\֭[gmm-r+ծw}599b1Чe \L333!%**j@ЌGWADFKMMď?f͂9g'OJQj7ٲX t3!oH6)+u<͝;W&>A ]!s؈ܾ}~/'N]QjGzWyx@gSCv_yÇ9{ׯ_bŊzlwvvӰYbC@C ,xӧ$kjjW_}R4__/ atE9ǫݢE5lܸb1Чrrr֬Y?_40.0V<(駟ӧ;`ω=O?otwwG4 UQ0q^uD_Q` FMNb˖-k׮rW;zoEokk@g謉*%%7v`)22a+Y,7͛xgp77H/HL ^ث!&&;|G;b?J7nm333?ѣ;::>ooÇٳ.\8u}3f̙3mڴ͛7!{oڵ/8Y,a9666 ߿W^w=sׯov;99 CCCzjt ͛;w.B/_.--eX,࠿?q_ t䠹_~ebGLeoݺu۶m:_SS@gX t@ZW?>8|0~$KZWK2Ё5k(p????00?ܲe b,bX,bX,;v?Rfdd oGzzzCҡT&ή緧Pgg˗k… ^rE=+4+CҥK>LD+N'O=_,zPP6vڥE||<+^ڞ={BRRRV^9..āw6v!olllll} SSHJNNNA!!!Νxb]]lkk۲e @VQQ]b"H.vssCLi"rD֨HT[nB uG@6mqㆢq\\\yũSѱ~zԱJD[ ?~𚕕uVZ2[ZZLLLɈZ#e #&j:D^2jQ__*yE9Hܽ{7)@uЌ؋#7(R@d48Am8;wDH  96uab?@ʍ莕cX1NH[ppghƍn!-@V`=E۸ii,D7 yPH&n]t'YD`(uuu"ӝ/ґK@`Qjjj(,6ŇG1***R+P$72;::jxra$%)±cDŽ F8d#j/4!M` tQ.%EDգ.Gn.nݢ, RɁ(B(htW}E@Q,tJ#TLd(`+ noq%5 .llZ[[=<<`a:mf~: A[A MHQ*,AtF7h(N(N0 ыP Je CM@OMMF Ҥg={EˈÐ^$JeZtxEr"<ԫ q2="wFFFxx8Mr3%(\:ĹP MSUUE@ iE7o6.u $$$v2CBBpLxX ,t {n$dP|ްA{q 07wʕ+i1a'qA @QБȒ%K<Jyx5r##wP ޴iZ r$%ЌhxD d(J6ALOOϝ;w*, q^ Q~C Bd E")(7(\F:׮]۰a<"Rr(N= (80D+Ѽtz>3u+:Rh\t]ܠD#|q|Nܽq#3b>LWYYu`;&9\#*ptl#sf"\-$fX,3ixo8~!ѤbU~6ohXu!@H+{,,Z J cKaK⃉&΅[gDo;3Y,Өjv KQv @Gg9RRmI@O t@gMaWޣzޯ7'[# %uǦ\ziyfjjjRRҥKGFFQ>ٳo9v__ܹsQ;wXXXb=D)Y}j衽 7A|*xUqttTv {+$Nex!;L,@1Y-%-[jkkQlq ~) Һ:!%___1,e*]}RfY[[͛g'E%G؝;wboaa+Kx$P~GՂ+w>ѡ;꽐X N=(Çq@ᦤp 8܊Ȕr@'+u&I nu9J(g $p ~ E]gWT)Jp(626;&j<訪d#1##Cgr*OSSSp"Lxc 0M%@ޡW*N*;꽐X N=(& g&V \:n~ o>4UDyz.g $${rd]]v_*U@rл GS@'w׀zKIs".|3U ɀө @ރ2Pa@-ncHLpi eyj0EWWWE(@&ŭND"]+@,X@X. E]gWqP*+egG, ŃФ :@/))F`NN.\S EtzM"yϊ.g\UT\HN^H,:ː{% S->> (?V=bX,bX,:b,b?JJJZd(W^M/QXXX\t$++K]… ϟWt4diKlk>\3<Ȋ,Oduօ2|2N*dSk"nzShN4?m35uȷypUkm67~- t=;v,11177z<[h9ڵ ;v0w9VOvW,h[q7ۣ77P8bOTF~_Ț9*,Q1b$qr9`㑤'd+Tyu]+e7QYdWՖuM)Za툉^/ j8vٿT׻!SVbךrE6NGjj­zWUL: jޝh)|~6W "1_t֯lugxER %rVeR^Tn$u©'s'ػ/Ajz7&&@WgSp?sp W@qn9;?_T4+%ˎ&ø>YDγnE{,gy}/ +mwƵ{6O hU]SZ׵RvEvU]txxXx Kzg_ŕ}y 'KM:%X{SwWz ]vEB>Aznջ bif]pE&ܲۺ{q}.R`)g:VQd1Y ;aр)+1 trw/aDk~WVJj~OB@DOa_)Eo@FV9.f)PX0'! J]vE-]T싫p5Z/U4){]{SJ@W8R'wV*&& '7~9 FGK@VU}hXmr:xKG}$Qk? / gË+KŰǿ4o}hv X t/G 0tcԕ'S~<+P0A_*7irr2 tݵ7eG_Q].U@N!fs=5ѧ?gXprԽX @WqՂb a}[n9s槟~nݺ>`ƌ4n2>}ip&C_[nnn|5,bX,:b,bX,bX t֣URRRPPPSSӡC"'ШՖ#~8M)Tim6VWġ̜lCѢՆsT06o+O,] {M(n@WK3VR~WpJ1]Ν H}b,B+nWHC.bVyjYnK„<\ K.rA<@ "[[[ŐZp']1BUWW6Bإ|Ty 2'Qy]TDmJS+WO]+ ?QTF@iyzzܹSt]>,=WT"hi~EW%W.A`jR2J[899S{ =,qv9$U:-) cTnև֤czzz0aAss?rgWoUo%5q)]{ߐiX,bX,:b,bOy2'^]ЕxC\@Lzպ瑔\#J#YMAq8@&~'>yFs\+i[3|䁙J+=܋8>U%;| ^CqԒ!D>j3;;{666&&&j|.]:xN96eOQj֭^^^(~@]>a(b:u?))0GGGZsll8S.ч鶆h Q}EydQQ`t?K8@R̚>K&W 3ہ\N:iV+ˮVЛ 6u+jY"*$)*»KM.b6 -?I In4q.Y[[ⲛO8X^+=ZTPNBS,:ߪD WL!wm|6}-V=y5z!Q|+H>)  v9$Sk7N V.гYDm)Dh*`1g^>ayQQQDgPD-;A)`رc8D: Fo+ &DAnhhPPK\(t*]vj>J!OiG v =K,K:y *:u-t0ۺL}m6pw[mљMDat6 Tr4=2 PT^k!κkY_/Ql:B S\V%<`@>.pql9.8#$|v‡D?E1/qt)dOQE"3 @9/QlՌ11"&$C(CCC C%?Vx^)..m QSryz.;*f blS- \zE쾔 {,Z:a,üewQ@C*dBE@]iL!9JIQ R'1Vf 2ةvHCyr[I 8lhohz] q}OS";>NpҁbD^ tJb >h%{*.{z @9/@Sʉj"{ӦMCārruu-#LvDرcz[CTQ}Q/{U8C.5@.sO (هŬ R؜[{GJ;F&lhy67vZdj\б_)3bSL}à[MbX,~mb,b67&uiȲ2= >\ئ_VMk?`y~YO/&΅y-1MWpGXC|Dv 8VV&AR>d__YXX(/k.d6ILvS\XrHd4e'Oʜ$#\F9tА3Y,cdVtڥ`l2ޫJ)h*mjuzk˯+R_>UNA\4|[gK|:-t%/XJeIԤV=Υ OZg.%%Vq]KSvT̩־.{ TJϯʙ,1?f|=mdgQy*;}?I@+G4#!< ǀ 8"sŇF}Gxk)} O@~! bP.B-9~]ɦ^:닒&p.UdJgddTkiwT8Ui=BEʙ,1k ΢Tbj XCr GFtrֱ-T Ӿ)x*C.豀uX ?6Tܳ$410Wɾ9X T/]>D ĉ4$+$tnr'p.Le]KSv~P@oLOr?Ё!a΢Vt@OJ44V)t%=UOY,:zV; T1+%4)vŐuG@])ϤbX tb1Y,@gX,:b,bc~GPȌ#nEb] ]t~6{-[\jPX,*(!"NBnעљM_ܴ6y5q/3<6/x9Y6:]^Ŝ:F6{jH~yi]b?r+_* ow09:q)R&.~̤VCVenYX/:TPON=.a)_X,>9uνXj%Y-+V?v]}#1M =]^103+"X, `W > +V'Qzaݛyӄظ_hccj#;/eX,:b,bX,bŊ^zV}#}VVV2,>{M>}ttb4?n\hQS]3==}W`O6LX t"\R.. V7bd#/${Vc@Gڕ"ǽ(s=s }رCĴ ؏>H]`|ڴi7Lr2Ay@/z4-tjlun iy]Q,uO,U],Y2u-tʴ¶NA`=W@|З-[?y=}-B o:dcc],--ݸq{p<@C.1Yޑ5c'n{\NmQ,uO@rBtNC.cE0;sZ9 o|u7q.Lk!׷nhjckjTjR!_~\>:nNg݊ b2㔊TXe+߁ri(ި]1Y,]Z=@u.|F&{rm \-$#kv#yn=Ã*Ya1=Kta+Ǘbk^t3&{y,ᷩUC@V5_~pX_T^Nx*pĹUa _r(}@/؟T9| D[`"ml͛ 6E*&RfúSc<« *o84KFddd>tF4ӧrHLLLRR_,:KbC3TcimVg0ǒDؕ@@|2Z:?Z*O@Qf{C5a?'cG|kAjB7n#o KmF!kH:t.Ʀs]  gO]>'.=tʹpț(^l qD&\Qt~ա14Dx ===nkk{ .tvv;vlttŋVٳ??~ʢ{-,,2f1YϞ={֬Y`[=}#dj-I/ҀW@yST /DW v]gWOӋ;ϺhO zZQ+AaO2|x/.gό-QzGG'}."H\~f9}:,(Qd=D]^fipp0_722Ü@W$,ɉ$aJ2u+"Dg6i{[d )9^ZZ:qC*$;v *:zUc/ ()}EOT d< j:w _)0@h9`YS_ZEeVdEm GНn5 BHa֎Y:6*++MLLDO &9vݸqls]]]AAAd@WkjjЁ{}\Qݴ6>1m.L?yЊ^XyW+;t W9KpI'͊E0iGqS)W 5t47tŽw'RбʮwGܜϝ;kL~{{ mmm6my? -*rI ~{Fs`u P{h]=5`)B@=]Qi x^5YeT9+G盙Ś04ƚ?~Œrm},Px\oa&H˞{PzՎq)ln>Tp.G/Л;XZz 詅mǯZGSqbX/Y,@gX,:b,bX,zY]\S!^b1П!W^+v^=Y0x>#Zjk|Yρ bMyi]MK+vވ7c@KmQqiH<=ztݺuʲ}$FFFN6mƌ .LKLam6!>>f͚uҟ~i޼y`zUZ}WlIP;OOOGpʕ˗/ӷם)w@R6/_… jmm}v0*++srrRk'پ} n ĝy%܍wî]/``yfll,4ի###GYr%vW_}5::B荍E.//߱c򪬬qCt!:nnn⧯/D=R'Op"EfffJJ ʃA v)gnnbbѐ#jٲe8 E.kkk;ڶG1nGG[ZZtG]]]Q/RS̵+*lF@ aPkse4//Ji44A üB]>p'pmGEE!_T d檕8I aÈvYtB+BCBB(k@DSLkddt Eo(?l=\]n:^ HɊs-l0~!O \:i&6 t*t1TUU'tNE'| g= N+ ^؃`xhrxLлB7onPM:WXAiz{{@")sN`~2deLt. 7P}on[Ԏ± (/PM| Fo(?@R+ptd ݃80i d CFC!_Őba4pnq8,UVBN4Ą<#Bc?dz-t333*X )N0,R>| FߢE`pSip r=^8oI!YaB]KIIɌ3_pS|V3.DYE[o )P l;wT74U A7:0đ5 ObE1׮޲:U+5N4%t{E=+Δy׮] i "k6I=f$/SΓ 61( kjjt짊JRv@G)vue/A rPx A[Ⴙa\\7n@փD.**a5+++eOKԈ痖&ꭸ`%WSɁ .* tz2pY7OU@8b1_ @L? {yy D3_!hkll 'ٷoKZٳNU=<X,QW_^iDe˖j5<`ƌ~8|󍫫+/\?}駈0gΜiӦm޼7|Ѕ\eX,a,tn}} t_-Z8 ݻa_ z hp&?bfffΛ7oܹVX芔 8=_=,>i o4ŋ;::e:;;װaYi`t}֭۶m+--a={vŊ芔 ( E{* MmpegkgIoԱ[BD88nw?G?WW J?'~er`mS7ۣGT&΅;}s⃉-)E;zuU9s{LOH`B= :0hByiPrECyg\7e'71D''U̸0YMj~`oooZߺ3?,X/%a0i]f WVV^`w]|g >C=00!GGGÜ_p!H0AQ#0:2:6cktja[y];Zf/>㑤.:tdu/;;u/7p +O YJ_5670?8 ޮyO j##<ϹkHaQMa(QZZtsvǭ8XR]Tc*@e*wt@e^:h~:w<|<5:` xK|  ,L[XXXw &VL &&&@1cpEE88vsS42" PG!<'[JEccѣGq@5HBhzKԹL/@ٳgϚ5BXuWS:~68:Jm|q2sZ%n &7@ALжg:w.b(Yc갋>r}Ѥz:S_#k)p,3;b]o{&vtbWaeWPbjM@V@,iWݳs @+\u2,t@UNB&ꢇHyf`)ǛXX1.h)))SG-Ȉ<///dnnnoo/.ooo?q533C6449sF~ݍb1~u[ū V9vۋnmӋnR *hP,.Hhr[nA !bHBBH !@O=uݞ9/w{gΗUәrՓ h/|^`tKfX:[o2 K:vi))U$)((p:lwyy9;~jjedd+^.-d0J%fT#Ho&B^?Y(p6`Jf-^pnXJicO/6F %GL ]E!6񺭭l6@X,{ L/f$)'NlN{)H$ t?7k. V8f &ezų-{\ٯ/X?jvkW544hg#nx455YFD)2 )hT,`0|N@'0L5jO.o𪩹qd,0E];NWOnRvs wlkꊊ  D"JŢx\K~> @*"rG.F{ D@'H$D"HtD"H$N"H$R?r\lM\ 6)]D@_@g?[,7bb6#/tkV+%cԻ$.MD܈Ţ.ow f{m;zD"c[oEv{k'Dam<79tzD"ГЯ['76bQك+wI$;7C?ʛ2;$e2"_d?==xyKHǔ7E5w=<G>~ڿAޅi_ إN3d]s:ʏ'N(@Ygy[=1·(>J;ny_eXeZY恎t|ryLY8 bxC!(^\ۼ@?/333R)vttj2@/..t0-///--b]]]YsssYYGYYY;s>X___YYH$$''G(_PBuD@СC|Y`S\LKOWa a #MX.md9@Cp]OBĘ A?w10@*vOo -~p?I jaQg`tA"/P'>Dh%~fDȻB8nÇBl[5&@gq0p:ѿ!\uQ |c`c;(CF;~>u:---'n pbb"//]ZZDF!0~? F yb1^\O1UUU2Gp C$lV8 ,ZWTuD@GX)&.s:'P$n[ |0nz߮ZkOz.^ǀh GQd#~5qpl)3[ -2=:`_f- i}.ؓAUL;p#aL>/%y޷&Vf%9(d ch+3{sh?iV09 q؎fiQ_Зv4+hJM(C{VLLx䂀,**bpO!xE.<"fH?=A .<]sĔ(_޹sGpd2lWSN}hhYH =οoIENDB`webshot/man/figures/r-small-resized.png0000644000176200001440000012560514225315117017676 0ustar liggesusersPNG  IHDR"c pHYs  bKGD%IDATx콇wUG;[sowo3kgn8lc l$!2!$,sY(r9zs>T> 6`[guN _jWoZhѢ'!M-ZhkѢE t-ZhѢE-Z4еhѢE]-ZhkѢE t-ZhѢE-ZhѢE]-ZhkѢE ֭[7oޜ~セZh@=::?\Y]SV^YTR(/0ZaqqIiiYEmm}sKkoo(&5hѢ~?p{pp:(jNJZfbrZ|bJ\Brl|RtlbTL|dt\DTlxdLXx4بںy Nrƍ 㽽S[n-?õQ켂ɩ) i I1q1 !aQAAa~!YY M#?֘ t rrre NyH;s{#iOSP M (]L(Ȃ)s<@) yo+>nWֱI^f ;?{7ˋ/KoKv9z(Lygׯ_Wճ> /_N$ sŮG+~嗗Ky~01/^?::zDGG^vdffо!-ߩ_O˼qhFZ"bBrmn;{vNн3h?jkk⋧̲zȰ(бCBB`XhիW1܂+Fᅬ )۷otƆE͛7VGFFdXãNW\A`R'JC^ H m%ThhhuuEc,16喝eMJb޽]f;b< -񜛛"-' JOΑq-Z.S1KJ3ry7|-h?ՔԴ|:./{8:98::$twYYYAy #Ǐ߱c%>qr#o>|>4ؕ.]tٳg_~>S,0Kw} Yj?V\:x &39`cc)%&&Yӕ![lPdPK,R8%%f ?h`RRpo~Gcbbad^6)Y}ƌhiyv /oKJN`7C`P,߀ V-tww;}Æ ś3Ķn:!&FvsV@1~BMMM@Ӓ)%PG@X$!&ynn.-_`Ϟ=3L;wRS3}ZLV\D. X'8JF[d^&եes<6>T9Aeb|5>>9[Kgm/FG'19s挼+..ЀNtZ1a@2YE4tqsb kbC| U###dnɅ1N@__6LC6]\\t2hC2]m/7rWs~ȸʐGtMg\Ъ)7gd465Aphd""'mڧOm' 5{d|x={̙ڵ{6Nᦚv-/@ؼyPg-tO*ΝcމOo.^5Ppm*,:ow5l,"t 0摶C)f;j8ɗ$J8"> EƵhy>00[P8S {-P;:`ܜ>II gS)73OLܬ|iSܼ˝8m@Jl{ U-qvvl׮]{e"MC?W@|6$WƝ$11NӁNl t>|MMM2+)wȅ $j&44T^nݺU"Z<,@umkC3-h}a_KO}K'1N>!z>ѿ](<#qdls0='Ѷ+[Q裣O^^^qB1_yKq?~MȤ_ʬ!"峽44d` Ðpk}dtZQlb㓳̼\oJf c@sg}Lh'k"}kWi3Ш'l;|oFҿ?б/^7,,,,$$ŋ7o,j"7]"뭷H!`Dsitƍ!پ}ם>%55Hӆц_~۶m G whggccc"JlΝsSj4N믿. t-;{zrR3g:[Xt3h5|67i;36ѵ^)̓Ug\8zy{cXd{ѢE2!ELoȫ&O:hȵU_|!~]]]ah F+\d٦ˍ!QĔ ҈ʽ#Z<@omkO˸: Ŝkc!@W4DmYuaߙ>ms>Мoք?6.fp`ȑӧOWVV߃Cdl^trY(`_q¿YSSݻ1aCCCkkkg[60 0www\FX0iqWiCmobiivڥvPJKKQ>**Y  %fol#QP(&/l 8ۥ|[IٹLsg\>2f[ ZU];GX3гmnf6̟z NPuBGׄGVn878im}3EE%m52zttuQb Vd)5_slV?m:hHso1}dޗ&A g9VN}C?m#5|?6ʧha }`zWh\\'=SXX{/oƳ> -ZDo@-Л[L[ϴVL\RKK@ۃBo4,yzj71ׄA_}de:G㒬8q¶R?:^e۶majѢ}'5#+~ͣb WM@Մ躘ݞeӀ7:W|~2##Q?{B[k~t wtttuu -ZZ. x[ɥK(fee}gD=ѓf;?ЄYnj\7@=R-^V^9\C1u 3)|jo&@slsh΄nV'ӯރ۔ 6,XaK.Ν?Ob!,z6:ͧPEӮw޸1uq:`ɓȉ'Xd9991..?s)1F$ bᇲ1M aiiQXWW)Aj 9*rb0Ģtuu  E Hخ ewfݺu111---3eH7FƵЇlҥְ0JOO/\ Kv3 y)BzJ (CYyyyEEE%HcǎKX- s[K$D7-Vee%)7}gff_T4$+WN t;)%=4|6_@_|/-{2.z0j!L -wkGoɸU=<44o}[=ܢ6 ȸ6aAh9Pزel,#u$,6Cq9t,stHN6o޼ 6'%cPi0D(տdo:G*5+*gb!Z◬'Z^VD D)4!|R\t`+M&m[͓ >Ev!wt!\34!8͛:N63/]*n믿O2B ~?|8z0rHO㠋䩐Eb[[{|bm<6}}M<fOaZooղ~0i_8Մ_mANꞱ?ߠV'.7ܳ!&[[[( 9sð;NwoS:VN;BI?b ޳ga1? @.~z UV]~ͨ'*pͲ_Yfǎ6l G9y}"Q}#Ɗ;N/d)#hD_B抦lRi-rJ90ېi) ʄ$M)p>]6²;#$u"#%ݡ$EPmmm]6F+:} \d^xM+H%AUYz hkSw$%++>^W=<=|C22Z0pm.,k"O?ǛQ.ZևۓdqСnnW^N|Ɣ5eEj 2 85-0d Y1\u ðY  бd!oNr('̡ ъVDhN2XmMMM-h#8~@|dH>`ʹfDфDKf1ie4p. qJJ _9?DSe+K䵵[n/^4ru:'B4 xC[t1@'FȆz!r&q^(?{{famshe _tL|\j߷'>njĩGVt),~`#BU ?_~w:Gwwl` [bҊP񬬬<ǩi@6$$СCbx@1XBFFNX&b!uPF͞M@OKK]1f$;;MG4$8([´µ9ybe?lGʜ6GȈTىu-CIrU.RPR@W:.i|):-{26hMD= t G+c}Ag˗=]O{'.e764WW{q}/W~Es'g$&2Ze1 v-(19W s@DžOҲ%,:-z65=""BgYm0UI Hr;:1Ϟ=;[J)eyqQ5ts._lqQXݻ@7BF08M6qf:] 1ЉP*ӧO7??_W;[@@L¢hvl{^vprrwptupp9q?6jjBcox%'Nؾ/4Čr>X|z߼Y'bMM@A&FR39~ߎommz-m.hƈ8z,g_ـomZjPu2dooYJ~[Ah(W>G[@Yndb4`)pt)" T`t\gȔwea,8Dd1mq+@///WTob~y zm/]rxY?m oք׶y8~ر3G=rz>?\h~AxjIǽ5חXQQdشHĖ19˩iF>ȉF4$5ntڄ6dfTCfXq )If hLH I w,\ҤчVHPl3e datRRRD-a1 t9BczȩS(nyK/aゞk׮qS(:#r@#:%)c666z ]֩--E11 >>.^0?m }tGV.3g\pBWcBh|߿tfؽ:XL R;U׬Y#NxjveNN/bˤ #ЁȼyO?TLC;۶mۿ' ںu+Q|Ϧ$@gΜVVVɓ~amm-mzJÀ2Ǐ!sIQAd,e¤ظq# I˻S_pV3@rjwLAiw^)7 ! Rv s rppY.4$6m"N=E>%;4<>GO-j/W;NP[[;>>V6 ?[v6?7}|k=je ak9`=..N xjիxرcl- &PRԘQ r1dH.ۋy9&Dy+@z, x2VPϋ={ iOnMxYq #\h6,,ty#-+ $5Ce>>0ɯ:qo2"8Q|8"krR=9m:m%%&-?4w?y-_-/=-^>׫~~?_;vյ߻yyyW\ݍK 'g?5 ClY4kjV CqrZ嘒"#'ЙX54G@@jo4a:aN ;;,**"(!u4>GGhH[bIFzF >4YYYj☟/ $2N[ [Y[[*s7$LQqѦ G^?$MZC"oLt $|Cvv6gee% Z4;i1=Wl~ϗXOB Ȩ=/MެޭSԱj2xDKwkKX8/!fi=|[xD>޳sPUWWRn޼Y.\@OПп{#+z~rͪ~?{gK+]ѥ-wt+466.\PCۦٵkކW~;~"䑕5\'+uil@o^x0鿿/q\߹6spx\?4Zo~,[.]%W\ s"hߏ;D;ܙg4~%tpl|G>6h[֩O?LLL tvvvtttww>O:GzG t7wh2}昆8:~FSPg)1hѢE]-ZhkѢE t-Zh@עE-ZhѢE]-Z4>0<schd|->#w|t|"Of-Zh}R|eؕ~ Mvܼu݃_\o <5^ЬE ^#c*zz/Ubt&W$W65A|r9&V6}:l.o-Z4nQPPeYӘYY6VZ]fxri;Ϫ?\h]^1cеhѢ~}xl!wo'4[Kڠy^mwPvSbU|q+(Ȯ@עE >F~m81t-ZshYs_Hm oQ>5ߜR>8]vuuuԡ!_,>@+bcc?p{yyהSNAۀV(iii#W1 H['xv|9rdgϞ%B P3m$ f888<Ӵ)`Cv hkѢ Ɍc2_Yp-t\#Џ?~':q됚fߴ. mmmt bccҥK/thtL744X_j ] X,fdff;۶m7o\3Kddd^^0o׭[ /Vc 1'!f˫O7ݝK޲.755Ž V@=@=fbؾ};>J||<`1ϟO-8t(_~K,ǽhڵo={/ݽ{BX\$*xA$TOnkk^{-,,B%fT*IsfaӉ6@ӟFe˖qI '''؍Y.W= %@fiTEg϶RK,29]&B_#e^Ooo@ $e Z<J$JݰaCuug}x+V9rsժUK.\rڵk,Y"o/%%eٲeܹsg{{ϦM}K.'xwy!-Z41'͛wAA-[n?ٳKPiׯ_Ǻwuu1yyy?O#A}'i⋇BעE=1{=GG^dgg?p|_v+谰_|@g?3 O?%"! t-Zh}1oڴ :n۶ ._<##˖-X>l@@.VիՐˮ]6n.]j֭# Zh@2,RT/vuuQla\(166F ų(ĉ WeKl%ZhѢE-Z4ncft:NfYu='OLL䤤:d+ oU8K-qRBߎn8QAylԨ4̦wQu緕r?IKK;q℻Nmmm}} @Raۍ̥%]߯Fd> %R ~;ydzz#՛5->e <HE I8liu5x!'"ev$_.:H=$% ?XA_WͭɵEa-P b&~@2'Ҝ$%%ǑKG2$-K%d14hn4 jHAN rĀi΍Oʠ q$wr([JPr\ÒU=oǞ<4u/_V'j@%>>˫"q|\P`` "RXXk׮*sssï\^o?3Ix"##UbYYY:R]]BƣG\LLIDlD1] aiU=** Pч8i;Fq#duŅ ŋ'I}\hPLAOb;s挨%$\\t VA'GE jQ}:$!<6D2N"/gϞ%@s_(47~xАvMqyle$wucO2K޽ziOpPIcJJ m T>cHh A {b dEKpI2Ij 5TQAs10 ^18#G=+ KBI74.$Jp \2Pܹs8, @VSI*._Z271<55UP G*p?%LM/jOAAADgrI?% ${a,|H;2: Ep$|3&66Et^Ph>nKpyb̢9 9mx'ȃ$AZp7<DB^(= O'S2RUU%ϛ2J@'4@I]9STNPF1 Öo\zKTT~TYfR >ʌ-N .@CUKͤ2<"Mf M@< ,hm(@'%@ t{!;t#PCCCxSo#p:`2JDMJs^Q RJITDB޹_()dJ0/ـ ܑ 'K!$3(@R$BQޛ:-MJ?py69A$$?N! .F9`d\R@'DIx:O ?1tꘘtϕ#梞SO 9`KH=D |Sg UHF@'UN@g-p'HAZJz; d`R߈PF9:"*sF O]\ă`#LJ##z)8㐋*gAbC7ʁH:u x(^B=[$΄S*El[Z_xH6 N!3C9\c>|;E3CF/P^ݼ<qPD%rA*C.4 EmZ)iTqIXRmLtP0ifx)@c(phEyrQ$C.h0HN yH]$-.S\?Y߼u[#7ncߦW7cC&ѱE9Pŗ8"nm~dpvq_zy(~&J F׭2O_^ rYS}Q%N*xÿxFFȱJI)| NOMwł3ƣ^*MT QUIHYr@yh,5NzYFUIHȸoCay1+=Y@X1Hpzߨ^_Jy(e2 Q%)C؋X*{Kcq)Ug҆0ȖjfySp ֫D+7?&zH Bt Rn[VWW:x1ίdn$u? {\\cpJc^yZ~Jbxv`o+ЙN&_rf-J{ϰsq]1l#%u}EgMxF^(y⊆~غN}whѢ~Iy}IґӮ;zF)e_P:tbPeVq1;׈>Y%Zh@,Zm;0ْRЎy~³|s(*=W=j?Ϳzar(=xAA,bw^8J|ưjŬ!ΓA2V+2- w F)++kii1|"!߳Ƞ 555|աsd?LZH8&c_쎡U7F[mq7U3^3R4dnAEzKMye|wGuj%?l]~krj5:Ubo䍖N trrj{3A C$&.{* ^*^TYʷ,ujBrYTvd3L$@RW+eWnq'PCOMM4O ܱcGmm-IP0ݝrhjǰ,[w $/萅(Gɫ]ل+HVIɓلuuuɝr@ b,^.4DjjEjѩY.7'&n "Ss]nr-Y.&--8ҥKz???@CǝKX*Y7Y͘cS ҁd*UK@#X&e!̢5JBG FGGˌuGA% e"PRR"SVqUdZT Jɂʒ 5SFqΝ;Ap9<<4?KϞ={H |Pmmm5$*qRGIrCidP oee>gB\bRO:e\AJ oDhaӺ&9APK9e+"(Rf䅒 i|2/񙐐 ,E(b"jqZXHTW&@'r7U%NjoCj7 vQ Qɑ)y@gY勘Am1a8ŝA=Y Pe &jA)ND"9bŬqd TVtྠY#G4\RH{/!#ad&DmYDf}"]@ZΝ]b esy*By/%UP9TErLG"E3Fk\tE]Z#O%@I F ^,ߺ:LS-janLB:P0'r C) &U@G1IH6$uVe 1۷o'; LY6 @N:i!+ @"4W@'jA) +*[U@Ii(._#(@Sd[6V> ,& S+6gll,) >)jq¸V鲨(pkP eo]-*^ ߿5e7.l-5.1ZS-Y3P &^]Ɣz *UH+T7uV,e~ƅ *mzyk`Wb[$A_r$ dPRWq$BZ᠇H mE%^!P]8DSxGl]\ ZP @?HDM㐋Z1D2$hpaR,E) a޽&~qC ++6)fYhJ|˶3 EͳaADU*- ÇQIvyն^W!v}m,"NoJY?cǹ<unpZ&P&-חPX~q|vww % VU٪řsGHlj})ZU OYHi<{S^ks7ukzzs'zW<،ˀ#?26`_q9:65z"jAF[r)7zBsں)vp涠;qfHL=8N芬${rH.TlwwwOOxM}VbMW 3U  trrjjr2c7w\)me_궳*{1j\,LkT.ke8p֕غ֡ş謖^`u֧Z}!?N-hS~}xdyϒky. 2Ѵ40Y('4<LC6BCC)cǎ'm Byj)ey#KT?QZȄ˦gʱ2I#(/9[;**JN%,䎴RXo E'O~5ǿƻ RX*5̓cnQսC M/W$H +|) H$*\#j^.Jn,PɽNi@AkqCWd t-J<w:;AqA}檄r9mgHqpHNNK!U+dVʢSW̼^۷X+Z陼mg7pǾhu[՛ K"= [wR@F@b/6B6clj&Q,j,=IFdgg'g{]M)-S0`A LMGxJ#~G!6/ څcJiprHTu&QDDV6A86r`EIM@eI#%Y* 5FZҹîԕ.%97-&1=g`]n'U7 Уayp )ɧOFwyHJSTf V{ܣtS!=(q^SQ  0NYT ٦ 5Y}Ux*A??M¸"{ako3< HIMV+##G>ӧWUϋr,)>9NB9N9ƑdžRMzZaV6]K{1wSh57֯}>z 4PbP-_ i@-W@\UZ4пQLzc8Llsoyۇ3G}?`,4N'ײ >:裸9ǦS6Nט@\*篭9 C.y {x ;Ԧ7wfwu oJ {Guː YA.t*r2S|?76~{ }C1'E[ߊV*wF[[[fd0b!j(A*$_Y jJSWva|$ʇ>t},\N畫JhE$ri3ٛBI>rЫ QXKxX9bN˚U)XO2ݚUd27 8h>1"[A~E-nĀ^\3L X~u=7%X7(*dȥㆇtE}) Ec ,x{g}VmD_GYfRxEn8.?FPxWNm,zeb)t?dY)~O#Rq\zx`񕲼PY0O~P/Ee"oXLR`e sʛQ [Ɨ0=5 %]2݂,queyl*KDeXɡƥ1xM⥨Z;Gx6/5T+`Q rٛ ewLEٳ_Yt)8}嗷m /s7|SO=vc lRcﶾTEŋ 6p7mDǖ^3ܧ~n5Xdɑ#G@y0W\I?C]vFŋ%*~;ƭGDDPն-ؒ ?888++/D+ D%<<)P7 !i_E? S(={> J6r#dۃ'eK~ GN]]]L$Degg16\=zbmll. >*:6 @lL8VJSăڒ;ʁx&-J6F6Ud.*q[UCIJ3y9uy6lOMM*OJ w^F ~0_hMP@$ >A߿ֆZ$!CiV ڲ܁\3O,2FP- wn+70TA§80>Zԣ9Үi$'GȅFҠSm,WFKt!.7(OZRO:dG\1Mu?:cKZx b0gn@zx1r*TH٠øX$Aœhf eon PIpRi݊TrioO&D`SX}yntr k % [rIgKS!♒K¬Cet5@.Jp<+CR CXK) j߾}4@o2R@' ZQdҖp ½##r#ѢAᇆ( RFX M6&DNoݺ*Qt--)t?~@[:P²ͯz [C~6y $ &=чgP"LgwL;".,=UޜY9qVm``RcBnDy}}D^ywڵOmx4=|rq~yoyJA?NMV]ɯo dVH5Qbˁ;GA1kggG}DNC|lr!Z*6;`/*9| is*{ȐKT^ez Ld_95JN!m__Aؑk "#Gy߀ &!b@ Rbl* aE!ftqʐߪO@.Ő ]?Ր .Fظ2"$F()gH9%OqK0>DH@n:mxx`F.pEQYFoqwGő$Br}Y"ƛl+iiMM@z9#c\tpGl)<[-CK<@O-hiĮh^۷b!ȶ):P Mb6^X Ox6 }t.vY%y~卥qfw5p4Lo=fbȦ-޿k:ٗմml+oD9BPr%u23$$-eamf X~˱[rI27IZ^_KQ^Z|^ e_bw@ŸD"/r/ɂ6z[47-Xt򛒗c",n[֨RFC.)ăFį6ƅFm /ZɜC(j wvXrdW qym̔%&!ܥČSƗ(CB iy }ҳpuSZm2mRMlZO+3ɢ [9vpI 9N~g3h7"St9<@Ͷyļ=f N,) %4xrqUYr, =|ʫthx-VE-N*ʺ[Қ2LcuScr bn*o>DHt"jz=b:T.u/)w ywUZW:x>e>T@r7*8I6Aļ6 Udyee'eZqqq\\\VVL j^ZZ;x 2*8pñcd X4(55U`\\\d#>zU&!Upp0W& , ʬg<6Qq 9//&!!ϟAU"'i"6KFFw’/HMx Xx{\h---”/$^ѣ$¤%S!>>ߔV DPHPe-PC&Jcd/U'>q!UaaiMاI %5Zbꀦgt{T-چ{\K)hwSWT}{>86?ZGae܌*V6|z&/$!{bn[BnKx `/p >]VVOBªr1*:Y߀Mmn%BYijiJ:Q\r/W^zQC˄z\:R[ Nه @9s&,,,""B^(|9(R.C֧`b{{{YU㓄#[ DKKǏcB%C4tFkkk䇬QK 2$Sk5 ߡJN3t} D%k,_Nw^DJ %ILL$ъ XrDQΔV3( yM'YUUEZ@)2ďdYr~}5P?_ol]aߕغ~T*҄{lv랋Xv@ULݪƁW7oCmGiZ`)}ƧwмkŠ*lmNE@iMX=Jzq'_ DNַ%=방k[ H; ShX LjPDcp;wuXGܮzY]_~y.~:- Iȅ7{iT>b_ot*:q<o>eE-+--%L0a~*QPO(c3N Iut %+EJ$S3%w?^(͉\DHf!, BMY UȈҒy Nä1jI$z 4h]AҢ1ҨЀ'i[. nHuCZrnYk LTɣ׆ 3:+1Nly<`q׷ շ5u܀`sJb4/(냼ӀNȌf"*qBd:*c5 8[Jk.cDOܼh 7 s.@6-Y +AZrGSTXeZ*;sʺHy|6r WI:_yLOj4 ~SKN; kzoLڸ9sμݿ9tzKwgtg'k;8}nYl̾ } I]$}TWTu<[U^&:lўN4D1Hm`KTEx =YpYP (8b1_BD!/F} Es,A0XN BqM1x頲IƜ܌$`yDѓja+BἨEvT'D"3")f*[+znsI: S߄s ?0{We 7 ӹ㥨:P5~3"{6ubH.~@KT%/(zc,hO({Q;GAP[ CI!`[1ȎC-ejĸJA[5/vWuoȎXwbq";u [C$" /|C7n})*Hoyq6G {ṱAt5 p"E?5 "v*:WY[h䵵o'C]Qj8R`be] ZOEH+tzRBBhP LRQ%E*Wf,LIn=5/CKQ"ȗ삡$t :2Zi%.27XHċSU~) v vlF ~([ץuK^at SjBx@4iҤ=@[Wm`*j6^[eY%ʙ)w)1*x(^o\՗ꌵ7Y:jObyõ4A[Ҽ3U,Iy0QIKXH&.N{z]Y׆VQ>>#ݽzf6H0P~AG bun+l+yOLL;v,33szzZPTTJJJŸMUe "uuuɪl-ZZZ.+688xõ4BbHfqq:CbR¤/I Vш!?“&ֹzM[; kX&k\ ߍ `jfU͘V|04CY39vKxe)vep1$E :+^uuukcB礤$/>xXCG׋LHr[W=E/gggWUUKAs}}=`.'ֵ4u/%"'mˊ677S/uN*n@&~ڋ[,c.YV3l>i G5ÎEj^ ? b_e?O\U_7N˛P-DF9: "׀>z] ޱbYX@|uhq+:Ե1ڀreeP2RZ={V,UO;::8"Z*@n-h4%" ׍G%Xwvvހ!E:Y!I{(v Sjb4,XM v?\Ej wǨ%K55]6pSVg)L Ć>;dW6o+fo;>"0ٗ*D PʼnQgE_@ L\؜Auu= uGL&gaeR1urο;] tӗذap>e0!t>)&# Ġ^FGOjltLKGzm$VQª2~wpij7\KA[_nU'.ub{0m4i9Gszϸ L#6rEތr#ye#PxumQ!c?bjVUlRP1apYU3Iqz)bɬה&M Sh>ĵ BO*)teƸ+l68MˌPP`xI0b+ ԉ9?iV"Xr,oSiҤIq@<)cg3t=0R z_ \iB_E9ή1'ѫն] E9.A}NW܋Rjm$hYpiҤIK&M4 tiҤI&.M4iEbyI&1F+S1}s75jY\2ņٖFOciaeb=]X8F4i$B_YY\>&*;mn.7NQK\pVuZ{%2C3@łw]o^2-,,K"#p'@3׊4i$)[S*Vņ9Wɣ9??[ cΖL&@ij:4TcIϭkX,kw鲝̏-i:mllryA~(M4 .5wC"!2;İ]¼OMj{uoV%{[-M Pa%ќ/XLe߸ĿY*E&M4 :F}^69лV65z3ʍWiof m(6k[FnHٌ0Zo|1n GQ8[cr8gmB&M0?^#襕H Ϲ1e^2/ނ"&Z]L++lJ[G.~8q`=2{Y6ɂE؜AyH&M]m[8...NNNV]s\6M64i$' /¿ۿ=zliҤIg@W}'xbffF7Ue2񩩩SNUUU%#2999b䎎~eqqDIIIUg&M}fb}e\/;v>z({  ~ΝxǛqAg}'?h\[[Ϝ9}^{ӇJLL+9H&/P$*Jlo{ngw#k뚡83h\`*k7-˾l@OMMh4'&&78rXswn?c===|駷mۆ`'̓O>kK_4 {m@E];Xa m6u=>^-o'b|ڲgo_؛1vҤn=,\W|\cubRK&/0 3 "kc.+vZF  ~Ҥk3t3zGƭA ġs~cY-XpHZ$<?3@>>@^wop8\RR/~n4'OLOO ] G*--%" M)%b*')˖ZGZ|ؾH'2-A3+nCuxdYjI&=\g|W-x s(BxGcCqݬ 2y?&l׎{zt}C6WhzPl|W-uכZW% ޱ -|TYtѳzGŻoePYϿ459Sp>Ka*eo{=,^D"+++pX̵W(ɪ a$MISϭP^ p4K5s~ dҪ̔;8避SמawQtܫoHkol,hI{eKMK}96"h$ǗFe;_ޖQ"=Ά~FFy;Gn>Z~ܹbG-pTw1XDrA29e]cNV 5|p~Z̜ͫkjT7zwwa-,,477w8W\rz9=4 {gZy]v-z*~ܺmOWzusys@ ԧsgWؖ}LIϟh>9;Wx}6'ܤrjFx5 \pfTh>7&% w(:uh΄Hgm#5͙)=Uםo}^0D4-"]W,qE]E`> ŠCW\Nѿac;sjfC|N:mc.>bܴL.ϯ7u9Qu6 O'VG#؛1=21qnAgmc3.|p:p%E`'zWVLXO+l?3pE?smux֡E}ʠ|87)pl: a70Z<'`È6{vv|{{{aaaZZ:zMM^ɓ'pN\')"lJ@Plla䍍ayb7T;Д (K*24,z?ABy7x@Pq{+ ŽVνy ;j.ʝQ$ZH+ oZЛz:& ba Z8K$q| &t\A7S|G~PihA, -LMMMLL p=33sq$}222Bֆx:끁Gݻw Ӄ@}v<4 ;i>{q[=uf^XN){Я*A@,h4yh䆾؇.z6ߔ"]/7L`(TF 3޲ rM,Ksp t1{i%BL5ݱ)n\ĝ\7-&~%/G V}24D؉vC=ط}:8eie$O#$ rjg'7TGk48I@sss3^XX@PrC& .py||jBvK`www{<aRPRbq7/4]:m8[E1 &`e@QG8}`N0~h%^S%1j.cI76?_[[4i?zCcW&+Iwbal#'P5;e kbup1@X'7w[7E b x .Ih~ncp41ҤI;| teK]=('@6zGʺŌϬ1/ fn/;1<]~x^yDcsp.*M4i#s#D4 Z7K()tF( §/Os~uzylF-ҵ~_n4 )@O+wԘG$8e}޽UCI&a:E3ptZ$<"1Uѽ:._@`hJ"ba8^!/{J&MC tiҤI&.M4iҤI&Mcy6ʜ\cſtB`0:}Mv]__woHnka}}cC7+XjmNM2=zNDqqq~~~QQQ[[˗v\UUUfffvvv?|q8%%%nWH.^XVV!LOOSJ/\022!NZmd``\L&]mٱ_ .PKK-|SΎ: ,7w97wG}#iw̔_Ko4KKK8+3AFI;WTT RBN7ُ;''+@ 럈Mu ˛ .WnTb*^4q((V s߿hTn s4R*bZM].]#\qGp~{{ym6nHX_}DoRSS\W=ǎ{GU ɑ~Q~J>77'mnnXiiiTTWWX RNu'Uh4Dv@0??DK-D$.999IL`|RPLKfp6E"4͛U$6($:Y 煢Rq2N9S$YC"8)))8΄ ȅ"Ʌ9#'e'rssq`C]]T9''V5hs*Kp*8$HΚIC;wN}Q*"BgnLMMqKܹs4,z=ʢE8xHb СvA抝F7[nѳ+M;8Q+#?K5uԡId\ 2=6M~2q/-č/r{@s?~(C?ּ9G?3+TOrJ?<7<O~J!8GtW@GKFF%!SwO!IJJ_:t9u'!O> tVĜ*B8-!~ !5Pf[tvnRD8qA-T% !Y.I/%:7Zȑ#F#( jɓ JQ6*H9eT6lYpZ8|dTN,jJ8K H)*O aqcD`8N.Us}R/QR` /lZl - *.>C󷤔L#9 6-ijgeĊq{ŵyn\jg>[!&Nԍ:0Zps9sܜs3y{n7xܟ8qb߾}ðCv%/2^q:u p{ě x؆0q@! 6,OFp#)wOC,܁8\PHZ!)T G8XIa]L{ItICQ0t(sbBJBBp$+#`⧠) FKROHY5 \l] )Nۻw/2kv\bvv=xx/ӥ:JiO>΄> )ɕZ^x✒(<t52|rΟU=SlҼm^C^k92Ԇc3>TYamg@311QXVν$ `]d;v ~i>/s?[ƝɄxUL{ C[*H\s7PC%9):[5 _62p ,HԄRO` Ȁ ihdn6 KQ| (1,hc# [L{~BB-S0NKb\Qru5;'%%RIVDK}N㣳mڐc.xNu?$9EOi+*Ek zu"|w ()@ ^$"Zvīg,p1d-S0׉zKQJK}2 ED❄$EBGhx5|Æb(.~bp|)Nm}1UPm%ХI @q- ʿt5:$ХI&M;!';|?. ފ4[]]UGZƏw=[4yQp4i> ';$]G+iZ a^b Hcc:BV+ޖ |HjOOOwwIK+jH6`t:s{{{MMMBB@2##A*FZt:^W_UTTR]mܾ>xķ"BooI:bZ`zzz]]ىoĨEU\&ȅR"5jmmgWTv||]Wq I]|\(HHˆqbfbmT5-I;h4uEM\MFDp:v7)[# :sVi$""`"@m,K|ܽw^[b@>ĐKu%f{ff%P ΂00tEE0з$k10G|OҥK999b'#11ATԩSx,⊝$(ɻo"ml)))TGD[(iP}gffRM!fPݷoIko*MCdbÇ!WLH\1x0G6...??~\٤ ˡC`"$x"u:r+ЁΤ 9*c \ QdD2zj)V=`\h@(M|&$%쬩Q.^2 XDpW}.F-@_XXP *EW?4Lq-CDcJ-=O-HⱸةF< թ8D !tؠ:bgԑ V#$i%1+"}yK@v*@@.**#3K&.Mڽ6 Ugzz`&M]4iҤIK&M4iҤI@&M4iҤI&aZv=~1H$`UVj999MFNgX1DI&~gko?}_U{PH@yuu5 ... K.(Wp:'k `ħux?Gn߻ZCkk5?~ۜ0d^nN/|[ZyG쟜,fco ;c_~y̙_zccc?OmYYٳ>׿Äy򫯾:==K/8vt>x !mۆK=u;NjZB?3ϐ{'GK5 eƞqW4>{O)D 3kfu~ J5 cumi!󎕼Y7(7-RJIpEF7ڏN`d|`%RhTbE5uv&M+1ʯ7i/Gu~ZkΏ8TF`QTwFFFp[?KKKF-fa6%%7c~3& ăW.z Xk4F &''ɓ'ϟ?裏WG߹sk(X?ٵk^@7x?~ sE}{9ҷ#i_uXA }ںfhc] WvXkfGhmբ7/]i2e;6K.6@CJ^;g݈[;g|dl=o# gVuZr$G_iw_g`ҍ'/΍|8*i>Rh&i!AJBEU54ƟU[sF ^ j}`3;;.՛puFȎ0ŭ6zu.IK c9gmN_R0ͰFi8 7MWl&ʙ=cu .TVV\b:bĩXYӧ9 f}b嚚tC c?ט:i eP0"D̔Ibd)֡+G}uaeSrUJZEE_1S$NRQ؉V{Pk!c'܏yyy~(h&vPS#u4iBGU1oO<XOJEzr.\R\ p%'' ~+1o0IH}{*$NF/5O7 #Q{xu@WI׾ =u'ZGs'Tdu=vu }0U! ޕd,P$3SOh?pa<ƃ3}".\?o#Oy1I(?=H(2k*ٗs!{n߼/s "Syɶ鲹% ~^,@||4g:x"iŽUBo?lI.4CWA rGzU@6##`]71+ŋO12tsss#%iTvv| 9pkX,0TxxxCCCl͞Dff&e>r,zq6䎫`k)3IQ 6h&ׇb'Ul W^ 3t 1P#C?sK/%Q R@e=>uu\$ bl\C(@phw??^$ynGpK9S.{<O,2S*I Qǩ(t@CF06>ys76⡼ɹo6xy_7qпJ=ã =G5w9BW/ڛ:FLz^:x-o6ƼQ#d>8N{/5 8|vFqlXmz6*ЏM  ++ 1؏#f:)>|XXhN]q(pDMO8t #ۢP8F򩦦ۦB<:|bG2*%D {*cDdz_d; X O;@7+F9!d:P4}ppޱb^X>;йO(xП}tڋGN9o?3!~>q+Tso꛳-SN@fgG<Б$Ku+ ="/a>t<߾1 MZid}:J'Nf') E ,ǯNb duSsB 8( UmI.j,dNhB[!9ك6'u.b|yśU#I0p푣@ &^&ٳgI\-@= 9}"V"Td"?vg谵Dc@y 1{rfO!xVLmx0 sbǍf YG Zt]x QX]ApZ4~%\?=Ka5a=<%ͷj12"W%A.Sx]{hNzuӗ)^po~¡5zԋd «-Ln.P-dOĢF)JIg ]#‹D:=!iddD 11(|MA ģd/:.!2Vns9E\WD,z 09[*tI5'~d+V?433gb=r?YLLHr'Z9 Y!86~bC$,^ 锥IK Zb+կK-{ҤI&M]4i$Х,Šjz^}WWWeex&C b=LUWWt"X?S\(M4 tij`odd$11Q755]|@qhttѣⳄ@2\JNN":@xgBɔ=&MDHVg WѬ`RCL~OzVV"YFG|wqv^x!ǹsoSSS_|ťKn7/>X,iiiKK/ǞH&M[ v,w"drp v(] ;ez rW ED)dGʗΰ at|g5~\Ӡsii)j=8q!o6cccW\B@ommEB!҂rܤWGKK&~g tYƴK &3W7|atטDcI.6!\ɮݕ6?38g_dޕTvXGmb+2fb7>}~GLOOA6,B CzOn\qZT׀ ;kCI&~J$(&uB f\jqTwg|ޥp^ݜóW?#9 = }ޱXT_{R2ХI&M4iҤIK&M4 ;nHQ/G3ր4iҤIMM/7&ۆWދnll,-G~RCQ_im_'X4i?#Eg|KƮ}rω Wd{u9|[U<_~҃-G[[sIonl1fjFŚ?,T1@ݓ`:X,Z:=1pZNglg^X>WlFhyNFb`~{2[^;P9sfV7WL▵Gzqt&tB^i2HZ_ dnДBE {߉ZWMeh(6%,oԩ:jfpCZt]Mt+@?`v{{{__-9`ʕ+NUt^cl6KK;R`Y:?Ye,Z6v$$hsۆ`;oH4}vsoG_:*쑵ubǨûfM"XK@.k^_r!|wh|!q!/m=:[@G˛h /NП>G5pAĹusPkw_gpөu>ynEȝXSZ:dxkD=G)O(-9|Au _&JD⊨q A#&gzB}RaѳJxVC0ҡM*e@['cY/g ԁ ꄔֹ?-KQmS I!_r99ȡsӧO 5'&&>,V> 333/^IJJ"ٳg6~Nwi+0uuuq낂Ǔ200@bb8a>c_,!5:EL?X@%I('½q>@77[{4 sz!,߶ Hn_U&fCtjĎ64)¿{̉p&>&i:nF j֦9)a{+~$rUf|S EGq!xv+$?9; Ѫ+kqU(3;;O5TN⧾hB1'?I")WGXe r 8 ։¹k_i [b387D^h2ٗ_ۍ̪Elw skя}֦˃m{UeW6l.q!56ſhS@z5mBQ"ͩ%q\ p'jCQB=я[j^UFq#G-%KCJPQ*IмONѼ~gyN:$K{b׷N` aCbv{)6B|9 z-([XVNF Ov"%A'99͎~u8O,Ы@ )XzѶJz IENDB`webshot/man/reexports.Rd0000644000176200001440000000061614225315117015026 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/pipe.R \docType{import} \name{reexports} \alias{reexports} \alias{\%>\%} \title{Objects exported from other packages} \keyword{internal} \description{ These objects are imported from other packages. Follow the links below to see their documentation. \describe{ \item{magrittr}{\code{\link[magrittr:pipe]{\%>\%}}} }} webshot/man/is_phantomjs_installed.Rd0000644000176200001440000000067613565533764017556 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{is_phantomjs_installed} \alias{is_phantomjs_installed} \title{Determine if PhantomJS is Installed} \usage{ is_phantomjs_installed() } \value{ \code{TRUE} if the PhantomJS is installed. Otherwise, \code{FALSE} if PhantomJS is not installed. } \description{ Verifies that a version of PhantomJS is installed and available for use on the user's computer. } webshot/man/appshot.Rd0000644000176200001440000000511214225315117014445 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/appshot.R \name{appshot} \alias{appshot} \alias{appshot.character} \alias{appshot.shiny.appobj} \title{Take a screenshot of a Shiny app} \usage{ appshot( app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL ) \method{appshot}{character}( app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL ) \method{appshot}{shiny.appobj}( app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL, webshot_timeout = 60 ) } \arguments{ \item{app}{A Shiny app object, or a string naming an app directory.} \item{file}{A vector of names of output files. Should end with \code{.png}, \code{.pdf}, or \code{.jpeg}. If several screenshots have to be taken and only one filename is provided, then the function appends the index number of the screenshot to the file name.} \item{...}{Other arguments to pass on to \code{\link{webshot}}.} \item{port}{Port that Shiny will listen on.} \item{envvars}{A named character vector or named list of environment variables and values to set for the Shiny app's R process. These will be unset after the process exits. This can be used to pass configuration information to a Shiny app.} \item{webshot_timeout}{The maximum number of seconds the phantom application is allowed to run before killing the process. If a delay argument is supplied (in \code{...}), the delay value is added to the timeout value.} } \description{ \code{appshot} performs a \code{\link{webshot}} using two different methods depending upon the object provided. If a 'character' is provided (pointing to an app.R file or app directory) an isolated background R process is launched to run the Shiny application. The current R process then captures the \code{\link{webshot}}. When a Shiny application object is supplied to \code{appshot}, it is reversed: the Shiny application runs in the current R process and an isolated background R process is launched to capture a \code{\link{webshot}}. The reason it is reversed in the second case has to do with scoping: although it would be preferable to run the Shiny application in a background process and call \code{webshot} from the current process, with Shiny application objects, there are potential scoping errors when run this way. } \examples{ if (interactive()) { appdir <- system.file("examples", "01_hello", package="shiny") # With a Shiny directory appshot(appdir, "01_hello.png") # With a Shiny App object shinyapp <- shiny::shinyAppDir(appdir) appshot(shinyapp, "01_hello_app.png") } } webshot/man/resize.Rd0000644000176200001440000000206613113313351014266 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/image.R \name{resize} \alias{resize} \title{Resize an image} \usage{ resize(filename, geometry) } \arguments{ \item{filename}{Character vector containing the path of images to resize.} \item{geometry}{Scaling specification. Can be a percent, as in \code{"50\%"}, or pixel dimensions like \code{"120x120"}, \code{"120x"}, or \code{"x120"}. Any valid ImageMagick geometry specifation can be used. If \code{filename} contains multiple images, this can be a vector to specify distinct sizes for each image.} } \description{ This does not change size of the image in pixels, nor does it affect appearance -- it is lossless compression. This requires GraphicsMagick (recommended) or ImageMagick to be installed. } \examples{ if (interactive()) { # Can be chained with webshot() or appshot() webshot("https://www.r-project.org/", "r-small-1.png") \%>\% resize("75\%") # Generate image that is 400 pixels wide webshot("https://www.r-project.org/", "r-small-2.png") \%>\% resize("400x") } } webshot/DESCRIPTION0000644000176200001440000000236714446417572013461 0ustar liggesusersPackage: webshot Title: Take Screenshots of Web Pages Version: 0.5.5 Authors@R: c( person("Winston", "Chang", email = "winston@rstudio.com", role = c("aut", "cre")), person("Yihui", "Xie", role = "ctb"), person("Francois", "Guillem", role = "ctb"), person("Barret", "Schloerke", role = "ctb"), person("Nicolas", "Perriault", role = "ctb", comment = "The CasperJS library") ) Description: Takes screenshots of web pages, including Shiny applications and R Markdown documents. Depends: R (>= 3.0) Imports: magrittr, jsonlite, callr Suggests: httpuv, knitr, rmarkdown, shiny, testthat (>= 3.0.0) License: GPL-2 SystemRequirements: PhantomJS for taking screenshots, ImageMagick or GraphicsMagick and OptiPNG for manipulating images. RoxygenNote: 7.2.3 Encoding: UTF-8 URL: https://wch.github.io/webshot/, https://github.com/wch/webshot/ BugReports: https://github.com/wch/webshot/issues Config/testthat/edition: 3 NeedsCompilation: no Packaged: 2023-06-26 22:07:00 UTC; winston Author: Winston Chang [aut, cre], Yihui Xie [ctb], Francois Guillem [ctb], Barret Schloerke [ctb], Nicolas Perriault [ctb] (The CasperJS library) Maintainer: Winston Chang Repository: CRAN Date/Publication: 2023-06-26 23:30:02 UTC webshot/tests/0000755000176200001440000000000014314364462013076 5ustar liggesuserswebshot/tests/testthat/0000755000176200001440000000000014446417572014745 5ustar liggesuserswebshot/tests/testthat/test-url.R0000644000176200001440000000164214314364462016643 0ustar liggesuserstest_that("fix_windows_url works properly", { testthat::skip_if_not(is_windows()) # Should add file:/// to file paths expect_equal( suppressWarnings(fix_windows_url("c:/path/file.html")), "file:///c:/path/file.html" ) expect_equal( suppressWarnings(fix_windows_url("c:\\path\\file.html")), "file:///c:/path/file.html" ) # Currently disabled because I'm not sure exactly should happen when there's # not a leading drive letter like "c:" # expect_equal(fix_windows_url("/path/file.html"), "file:///c:/path/file.html") # expect_equal(fix_windows_url("\\path\\file.html"), "file:///c:/path/file.html") # expect_equal(fix_windows_url("/path\\file.html"), "file:///c:/path/file.html") # Shouldn't affect proper URLs expect_equal(fix_windows_url("file:///c:/path/file.html"), "file:///c:/path/file.html") expect_equal(fix_windows_url("http://x.org/file.html"), "http://x.org/file.html") }) webshot/tests/testthat.R0000644000176200001440000000007214314364462015060 0ustar liggesuserslibrary(testthat) library(webshot) test_check("webshot") webshot/R/0000755000176200001440000000000014446173470012140 5ustar liggesuserswebshot/R/image.R0000644000176200001440000000614113030514600013325 0ustar liggesusers#' Resize an image #' #' This does not change size of the image in pixels, nor does it affect #' appearance -- it is lossless compression. This requires GraphicsMagick #' (recommended) or ImageMagick to be installed. #' #' @param filename Character vector containing the path of images to resize. #' @param geometry Scaling specification. Can be a percent, as in \code{"50\%"}, #' or pixel dimensions like \code{"120x120"}, \code{"120x"}, or \code{"x120"}. #' Any valid ImageMagick geometry specifation can be used. If \code{filename} #' contains multiple images, this can be a vector to specify distinct sizes #' for each image. #' #' @examples #' if (interactive()) { #' # Can be chained with webshot() or appshot() #' webshot("https://www.r-project.org/", "r-small-1.png") %>% #' resize("75%") #' #' # Generate image that is 400 pixels wide #' webshot("https://www.r-project.org/", "r-small-2.png") %>% #' resize("400x") #' } #' @export resize <- function(filename, geometry) { mapply(resize_one, filename = filename, geometry = geometry, SIMPLIFY = FALSE, USE.NAMES = FALSE) structure(filename, class = "webshot") } resize_one <- function(filename, geometry) { # Handle missing phantomjs if (is.null(filename)) return(NULL) # First look for graphicsmagick, then imagemagick prog <- Sys.which("gm") if (prog == "") { # ImageMagick 7 has a "magick" binary prog <- Sys.which("magick") } if (prog == "") { if (is_windows()) { prog <- find_magic() } else { prog <- Sys.which("convert") } } if (prog == "") stop("None of `gm`, `magick`, or `convert` were found in path. GraphicsMagick or ImageMagick must be installed and in path.") args <- c(filename, "-resize", geometry, filename) if (names(prog) %in% c("gm", "magick")) { args <- c("convert", args) } res <- system2(prog, args) if (res != 0) stop ("Resizing with `gm convert`, `magick convert` or `convert` failed.") filename } #' Shrink file size of a PNG #' #' This does not change size of the image in pixels, nor does it affect #' appearance -- it is lossless compression. This requires the program #' \code{optipng} to be installed. #' #' If other operations like resizing are performed, shrinking should occur as #' the last step. Otherwise, if the resizing happens after file shrinking, it #' will be as if the shrinking didn't happen at all. #' #' @param filename Character vector containing the path of images to resize. #' Must be PNG files. #' #' @examples #' if (interactive()) { #' webshot("https://www.r-project.org/", "r-shrink.png") %>% #' shrink() #' } #' @export shrink <- function(filename) { mapply(shrink_one, filename = filename, SIMPLIFY = FALSE, USE.NAMES = FALSE) structure(filename, class = "webshot") } shrink_one <- function(filename) { # Handle missing phantomjs if (is.null(filename)) return(NULL) optipng <- Sys.which("optipng") if (optipng == "") stop("optipng not found in path. optipng must be installed and in path.") res <- system2('optipng', filename) if (res != 0) stop ("Shrinking with `optipng` failed.") filename } webshot/R/utils.R0000644000176200001440000003407614314364462013432 0ustar liggesusersphantom_run <- function(args, wait = TRUE, quiet = FALSE) { phantom_bin <- find_phantom(quiet = quiet) # Handle missing phantomjs if (is.null(phantom_bin)) return(NULL) # Make sure args is a char vector args <- as.character(args) p <- callr::process$new( phantom_bin, args = args, stdout = "|", stderr = "|", supervise = TRUE ) if (isTRUE(wait)) { on.exit({ p$kill() }) cat_n <- function(txt) { if (length(txt) > 0) { cat(txt, sep = "\n") } } while(p$is_alive()) { p$wait(200) # wait until min(c(time_ms, process ends)) cat_n(p$read_error_lines()) cat_n(p$read_output_lines()) } } p$get_exit_status() } # Find PhantomJS from PATH, APPDATA, system.file('webshot'), ~/bin, etc find_phantom <- function(quiet = FALSE) { path <- Sys.which( "phantomjs" ) if (path != "") return(path) for (d in phantom_paths()) { exec <- if (is_windows()) "phantomjs.exe" else "phantomjs" path <- file.path(d, exec) if (utils::file_test("-x", path)) break else path <- "" } if (path == "") { # It would make the most sense to throw an error here. However, that would # cause problems with CRAN. The CRAN checking systems may not have phantomjs # and may not be capable of installing phantomjs (like on Solaris), and any # packages which use webshot in their R CMD check (in examples or vignettes) # will get an ERROR. We'll issue a message and return NULL; other if(!quiet) { message( "PhantomJS not found. You can install it with webshot::install_phantomjs(). ", "If it is installed, please make sure the phantomjs executable ", "can be found via the PATH variable." ) } return(NULL) } path.expand(path) } phantomjs_cmd_result <- function(args, wait = TRUE, quiet = FALSE) { # Retrieve and store output from STDOUT utils::capture.output(invisible(phantom_run(args = args, wait = wait, quiet = quiet)), type = "output") } phantomjs_version <- function() { phantomjs_cmd_result("--version", quiet = TRUE) } #' Determine if PhantomJS is Installed #' #' Verifies that a version of PhantomJS is installed and available for use #' on the user's computer. #' #' @return \code{TRUE} if the PhantomJS is installed. Otherwise, \code{FALSE} #' if PhantomJS is not installed. #' #' @export is_phantomjs_installed <- function() { !is.null(find_phantom(quiet = TRUE)) } is_phantomjs_version_latest <- function(requested_version) { # Ensure phantomjs is installed if not, request an update if (!is_phantomjs_installed()) { return(FALSE) } # Obtain the installed version installed_phantomjs_version <- phantomjs_version() # For versions 2.5.0-beta and 2.5.0-beta2, phantomjs reports its version to be # "2.5.0-development". # # However, for the requested version, we have the names "2.5.0-beta" and # "2.5.0-beta2". # # For simplicity, we'll just remove the "-development" or "-beta" or "-beta2" # so they all map to "2.5.0". installed_phantomjs_version <- sub("-development", "", installed_phantomjs_version) requested_version <- sub("-beta.*", "", requested_version) # Check if the installed version is latest compared to requested version. as.package_version(installed_phantomjs_version) >= requested_version } #' Install PhantomJS #' #' Download the zip package, unzip it, and copy the executable to a system #' directory in which \pkg{webshot} can look for the PhantomJS executable. #' #' @details This function was designed primarily to help Windows users since it #' is cumbersome to modify the \code{PATH} variable. Mac OS X users may #' install PhantomJS via Homebrew. If you download the package from the #' PhantomJS website instead, please make sure the executable can be found via #' the \code{PATH} variable. #' #' On Windows, the directory specified by the environment variable #' \code{APPDATA} is used to store \file{phantomjs.exe}. On OS X, the #' directory \file{~/Library/Application Support} is used. On other platforms #' (such as Linux), the directory \file{~/bin} is used. If these directories #' are not writable, the directory \file{PhantomJS} under the installation #' directory of the \pkg{webshot} package will be tried. If this directory #' still fails, you will have to install PhantomJS by yourself. #' #' If PhantomJS is not already installed on the computer, this function will #' attempt to install it. However, if the version of PhantomJS installed is #' greater than or equal to the requested version, this function will not #' perform the installation procedure again unless the \code{force} parameter #' is set to \code{TRUE}. As a result, this function may also be used to #' reinstall or downgrade the version of PhantomJS found. #' #' @param version The version number of PhantomJS. #' @param baseURL The base URL for the location of PhantomJS binaries for #' download. If the default download site is unavailable, you may specify an #' alternative mirror, such as #' \code{"https://bitbucket.org/ariya/phantomjs/downloads/"}. #' @param force Install PhantomJS even if the version installed is the latest or #' if the requested version is older. This is useful to reinstall or downgrade #' the version of PhantomJS. #' @return \code{NULL} (the executable is written to a system directory). #' @export install_phantomjs <- function(version = '2.1.1', baseURL = 'https://github.com/wch/webshot/releases/download/v0.3.1/', force = FALSE) { if (!force && is_phantomjs_version_latest(version)) { message('It seems that the version of `phantomjs` installed is ', 'greater than or equal to the requested version.', 'To install the requested version or downgrade to another version, ', 'use `force = TRUE`.') return(invisible()) } if (!grepl("/$", baseURL)) baseURL <- paste0(baseURL, "/") owd <- setwd(tempdir()) on.exit(setwd(owd), add = TRUE) if (is_windows()) { zipfile <- sprintf('phantomjs-%s-windows.zip', version) download(paste0(baseURL, zipfile), zipfile, mode = 'wb') utils::unzip(zipfile) zipdir <- sub('.zip$', '', zipfile) exec <- file.path(zipdir, 'bin', 'phantomjs.exe') } else if (is_osx()) { zipfile <- sprintf('phantomjs-%s-macosx.zip', version) download(paste0(baseURL, zipfile), zipfile, mode = 'wb') utils::unzip(zipfile) zipdir <- sub('.zip$', '', zipfile) exec <- file.path(zipdir, 'bin', 'phantomjs') Sys.chmod(exec, '0755') # chmod +x } else if (is_linux()) { zipfile <- sprintf( 'phantomjs-%s-linux-%s.tar.bz2', version, if (grepl('64', Sys.info()[['machine']])) 'x86_64' else 'i686' ) download(paste0(baseURL, zipfile), zipfile, mode = 'wb') utils::untar(zipfile) zipdir <- sub('.tar.bz2$', '', zipfile) exec <- file.path(zipdir, 'bin', 'phantomjs') Sys.chmod(exec, '0755') # chmod +x } else { # Unsupported platform, like Solaris message("Sorry, this platform is not supported.") return(invisible()) } success <- FALSE dirs <- phantom_paths() for (destdir in dirs) { dir.create(destdir, showWarnings = FALSE) success <- file.copy(exec, destdir, overwrite = TRUE) if (success) break } unlink(c(zipdir, zipfile), recursive = TRUE) if (!success) stop( 'Unable to install PhantomJS to any of these dirs: ', paste(dirs, collapse = ', ') ) message('phantomjs has been installed to ', normalizePath(destdir)) invisible() } # Possible locations of the PhantomJS executable phantom_paths <- function() { if (is_windows()) { path <- Sys.getenv('APPDATA', '') path <- if (dir_exists(path)) file.path(path, 'PhantomJS') } else if (is_osx()) { path <- '~/Library/Application Support' path <- if (dir_exists(path)) file.path(path, 'PhantomJS') } else { path <- '~/bin' } path <- c(path, file.path(system.file(package = 'webshot'), 'PhantomJS')) path } dir_exists <- function(path) utils::file_test('-d', path) # Given a vector or list, drop all the NULL items in it dropNulls <- function(x) { x[!vapply(x, is.null, FUN.VALUE=logical(1))] } is_windows <- function() .Platform$OS.type == "windows" is_osx <- function() Sys.info()[['sysname']] == 'Darwin' is_linux <- function() Sys.info()[['sysname']] == 'Linux' is_solaris <- function() Sys.info()[['sysname']] == 'SunOS' # Find an available TCP port (to launch Shiny apps) available_port <- function(port = NULL, min = 3000, max = 9000) { if (!is.null(port)) return(port) # Unsafe port list from shiny::runApp() valid_ports <- setdiff(min:max, c(3659, 4045, 6000, 6665:6669, 6697)) # Try up to 20 ports for (port in sample(valid_ports, 20)) { handle <- NULL # Check if port is open tryCatch( handle <- httpuv::startServer("127.0.0.1", port, list()), error = function(e) { } ) if (!is.null(handle)) { httpuv::stopServer(handle) return(port) } } stop("Cannot find an available port") } # Wrapper for utils::download.file which works around a problem with R 3.3.0 and # 3.3.1. In these versions, download.file(method="libcurl") issues a HEAD # request to check if a file is available, before sending the GET request. This # causes problems when downloading attached files from GitHub binary releases # (like the PhantomJS binaries), because the url for the GET request returns a # 403 for HEAD requests. See # https://stat.ethz.ch/pipermail/r-devel/2016-June/072852.html download <- function(url, destfile, mode = "w") { if (getRversion() >= "3.3.0") { download_no_libcurl(url, destfile, mode = mode) } else if (is_windows() && getRversion() < "3.2") { # Older versions of R on Windows need setInternet2 to download https. download_old_win(url, destfile, mode = mode) } else { utils::download.file(url, destfile, mode = mode) } } # Adapted from downloader::download, but avoids using libcurl. download_no_libcurl <- function(url, ...) { # Windows if (is_windows()) { method <- "wininet" utils::download.file(url, method = method, ...) } else { # If non-Windows, check for libcurl/curl/wget/lynx, then call download.file with # appropriate method. if (nzchar(Sys.which("wget")[1])) { method <- "wget" } else if (nzchar(Sys.which("curl")[1])) { method <- "curl" # curl needs to add a -L option to follow redirects. # Save the original options and restore when we exit. orig_extra_options <- getOption("download.file.extra") on.exit(options(download.file.extra = orig_extra_options)) options(download.file.extra = paste("-L", orig_extra_options)) } else if (nzchar(Sys.which("lynx")[1])) { method <- "lynx" } else { stop("no download method found") } utils::download.file(url, method = method, ...) } } # Adapted from downloader::download, for R<3.2 on Windows download_old_win <- function(url, ...) { # If we directly use setInternet2, R CMD CHECK gives a Note on Mac/Linux seti2 <- `::`(utils, 'setInternet2') # Check whether we are already using internet2 for internal internet2_start <- seti2(NA) # If not then temporarily set it if (!internet2_start) { # Store initial settings, and restore on exit on.exit(suppressWarnings(seti2(internet2_start))) # Needed for https. Will get warning if setInternet2(FALSE) already run # and internet routines are used. But the warnings don't seem to matter. suppressWarnings(seti2(TRUE)) } method <- "internal" # download.file will complain about file size with something like: # Warning message: # In download.file(url, ...) : downloaded length 19457 != reported length 200 # because apparently it compares the length with the status code returned (?) # so we supress that utils::download.file(url, method = method, ...) } # Fix local filenames like "c:/path/file.html" to "file:///c:/path/file.html" # because that's the format used by casperjs and the webshot.js script. fix_windows_url <- function(url) { if (!is_windows()) return(url) fix_one <- function(x) { # If it's a "c:/path/file.html" path, or contains any backslashs, like # "c:\path", "\\path\\file.html", or "/path\\file.html", we need to fix it # up. However, we need to leave paths that are already URLs alone. if (grepl("^[a-zA-Z]:[/\\]", x) || (!grepl(":", x, fixed = TRUE) && grepl("\\", x, fixed = TRUE))) { paste0("file:///", normalizePath(x, winslash = "/")) } else { x } } vapply(url, fix_one, character(1), USE.NAMES = FALSE) } # Borrowed from animation package, with some adaptations. find_magic = function() { # try to look for ImageMagick in the Windows Registry Hive, the Program Files # directory and the LyX installation if (!inherits(try({ magick.path = utils::readRegistry('SOFTWARE\\ImageMagick\\Current')$BinPath }, silent = TRUE), 'try-error')) { if (nzchar(magick.path)) { convert = normalizePath(file.path(magick.path, 'convert.exe'), "/", mustWork = FALSE) } } else if ( nzchar(prog <- Sys.getenv('ProgramFiles')) && length(magick.dir <- list.files(prog, '^ImageMagick.*')) && length(magick.path <- list.files(file.path(prog, magick.dir), pattern = '^convert\\.exe$', full.names = TRUE, recursive = TRUE)) ) { convert = normalizePath(magick.path[1], "/", mustWork = FALSE) } else if (!inherits(try({ magick.path = utils::readRegistry('LyX.Document\\Shell\\open\\command', 'HCR') }, silent = TRUE), 'try-error')) { convert = file.path(dirname(gsub('(^\"|\" \"%1\"$)', '', magick.path[[1]])), c('..', '../etc'), 'imagemagick', 'convert.exe') convert = convert[file.exists(convert)] if (length(convert)) { convert = normalizePath(convert, "/", mustWork = FALSE) } else { warning('No way to find ImageMagick!') return("") } } else { warning('ImageMagick not installed yet!') return("") } if (!file.exists(convert)) { # Found an ImageMagick installation, but not the convert.exe binary. warning("ImageMagick's convert.exe not found at ", convert) return("") } return(convert) } webshot/R/zzz.R0000644000176200001440000000175413353465150013122 0ustar liggesusersregister_s3_method <- function(pkg, generic, class, fun = NULL) { stopifnot(is.character(pkg), length(pkg) == 1) stopifnot(is.character(generic), length(generic) == 1) stopifnot(is.character(class), length(class) == 1) if (is.null(fun)) { fun <- get(paste0(generic, ".", class), envir = parent.frame()) } else { stopifnot(is.function(fun)) } if (pkg %in% loadedNamespaces()) { registerS3method(generic, class, fun, envir = asNamespace(pkg)) } # Always register hook in case package is later unloaded & reloaded setHook( packageEvent(pkg, "onLoad"), function(...) { registerS3method(generic, class, fun, envir = asNamespace(pkg)) } ) } .onLoad <- function(...) { # webshot provides methods for knitr::knit_print, but knitr isn't a Depends # or Imports of htmltools, only an Enhances. This code snippet manually # registers our method(s) with S3 once both webshot and knitr are loaded. register_s3_method("knitr", "knit_print", "webshot") } webshot/R/rmdshot.R0000644000176200001440000000452313353465150013742 0ustar liggesusers#' Take a snapshot of an R Markdown document #' #' This function can handle both static Rmd documents and Rmd documents with #' \code{runtime: shiny}. #' #' @inheritParams appshot #' @param doc The path to a Rmd document. #' @param delay Time to wait before taking screenshot, in seconds. Sometimes a #' longer delay is needed for all assets to display properly. If NULL (the #' default), then it will use 0.2 seconds for static Rmd documents, and 3 #' seconds for Rmd documents with runtime:shiny. #' @param rmd_args A list of additional arguments to pass to either #' \code{\link[rmarkdown]{render}} (for static Rmd documents) or #' \code{\link[rmarkdown]{run}} (for Rmd documents with runtime:shiny). #' #' @examples #' if (interactive()) { #' # rmdshot("rmarkdown_file.Rmd", "snapshot.png") #' #' # R Markdown file #' input_file <- system.file("examples/knitr-minimal.Rmd", package = "knitr") #' rmdshot(input_file, "minimal_rmd.png") #' #' # Shiny R Markdown file #' input_file <- system.file("examples/shiny.Rmd", package = "webshot") #' rmdshot(input_file, "shiny_rmd.png", delay = 5) #' } #' #' @export rmdshot <- function(doc, file = "webshot.png", ..., delay = NULL, rmd_args = list(), port = getOption("shiny.port"), envvars = NULL) { runtime <- rmarkdown::yaml_front_matter(doc)$runtime if (is_shiny(runtime)) { if (is.null(delay)) delay <- 3 rmdshot_shiny(doc, file, ..., delay = delay, rmd_args = rmd_args, port = port, envvars = envvars) } else { if (is.null(delay)) delay <- 0.2 outfile <- tempfile("webshot", fileext = ".html") do.call(rmarkdown::render, c(list(doc, output_file = outfile), rmd_args)) webshot(outfile, file = file, ...) } } rmdshot_shiny <- function(doc, file, ..., rmd_args, port, envvars) { port <- available_port(port) url <- shiny_url(port) # Run app in background with envvars p <- r_background_process( function(...) { rmarkdown::run(...) }, args = append( list(file = doc, shiny_args = list(port = port)), rmd_args ), envvars = envvars ) on.exit({ p$kill() }) # Wait for app to start wait_until_server_exists(url) fileout <- webshot(url, file = file, ...) invisible(fileout) } # Borrowed from rmarkdown is_shiny <- function (runtime) { !is.null(runtime) && grepl("^shiny", runtime) } webshot/R/wait.R0000644000176200001440000000155313353465150013226 0ustar liggesusersshiny_url <- function(port) { sprintf("http://127.0.0.1:%d/", port) } server_exists <- function(url) { !inherits( try({ suppressWarnings(readLines(url, 1)) }, silent = TRUE), "try-error" ) } webshot_app_timeout <- function() { getOption("webshot.app.timeout", 60) } wait_until_server_exists <- function( url, timeout = webshot_app_timeout() ) { cur_time <- function() { as.numeric(Sys.time()) } start <- cur_time() while(!server_exists(url)) { if (cur_time() - start > timeout) { stop( 'It took more than ', timeout, ' seconds to launch the Shiny Application. ', 'There may be something wrong. The process has been killed. ', 'If the app needs more time to be launched, set ', 'options(webshot.app.timeout) to a larger value.', call. = FALSE ) } Sys.sleep(0.25) } TRUE } webshot/R/webshot.R0000644000176200001440000002474414446173470013751 0ustar liggesusers#' Take a screenshot of a URL #' #' @param url A vector of URLs to visit. #' @param file A vector of names of output files. Should end with \code{.png}, #' \code{.pdf}, or \code{.jpeg}. If several screenshots have to be taken and #' only one filename is provided, then the function appends the index number #' of the screenshot to the file name. #' @param vwidth Viewport width. This is the width of the browser "window". #' @param vheight Viewport height This is the height of the browser "window". #' @param cliprect Clipping rectangle. If \code{cliprect} and \code{selector} #' are both unspecified, the clipping rectangle will contain the entire page. #' This can be the string \code{"viewport"}, in which case the clipping #' rectangle matches the viewport size, or it can be a four-element numeric #' vector specifying the top, left, width, and height. When taking screenshots #' of multiple URLs, this parameter can also be a list with same length as #' \code{url} with each element of the list being "viewport" or a #' four-elements numeric vector. This option is not compatible with #' \code{selector}. #' @param selector One or more CSS selectors specifying a DOM element to set the #' clipping rectangle to. The screenshot will contain these DOM elements. For #' a given selector, if it has more than one match, only the first one will be #' used. This option is not compatible with \code{cliprect}. When taking #' screenshots of multiple URLs, this parameter can also be a list with same #' length as \code{url} with each element of the list containing a vector of #' CSS selectors to use for the corresponding URL. #' @param delay Time to wait before taking screenshot, in seconds. Sometimes a #' longer delay is needed for all assets to display properly. #' @param expand A numeric vector specifying how many pixels to expand the #' clipping rectangle by. If one number, the rectangle will be expanded by #' that many pixels on all sides. If four numbers, they specify the top, #' right, bottom, and left, in that order. When taking screenshots of multiple #' URLs, this parameter can also be a list with same length as \code{url} with #' each element of the list containing a single number or four numbers to use #' for the corresponding URL. #' @param zoom A number specifying the zoom factor. A zoom factor of 2 will #' result in twice as many pixels vertically and horizontally. Note that using #' 2 is not exactly the same as taking a screenshot on a HiDPI (Retina) #' device: it is like increasing the zoom to 200% in a desktop browser and #' doubling the height and width of the browser window. This differs from #' using a HiDPI device because some web pages load different, #' higher-resolution images when they know they will be displayed on a HiDPI #' device (but using zoom will not report that there is a HiDPI device). #' @param eval An optional string with JavaScript code which will be evaluated #' after opening the page and waiting for \code{delay}, but before calculating #' the clipping region and taking the screenshot. See the Casper API #' for more information about what commands can be used to control the web #' page. NOTE: This is experimental and likely to change! #' @param debug Print out debugging messages from PhantomJS and CasperJS. This can help to #' diagnose problems. #' @param useragent The User-Agent header used to request the URL. Changing the #' User-Agent can mitigate rendering issues for some websites. #' #' @examples #' if (interactive()) { #' #' # Whole web page #' webshot("https://github.com/rstudio/shiny") #' #' # Might need a longer delay for all assets to display #' webshot("http://rstudio.github.io/leaflet", delay = 0.5) #' #' # One can also take screenshots of several URLs with only one command. #' # This is more efficient than calling 'webshot' multiple times. #' webshot(c("https://github.com/rstudio/shiny", #' "http://rstudio.github.io/leaflet"), #' delay = 0.5) #' #' # Clip to the viewport #' webshot("http://rstudio.github.io/leaflet", "leaflet-viewport.png", #' cliprect = "viewport") #' #' # Manual clipping rectangle #' webshot("http://rstudio.github.io/leaflet", "leaflet-clip.png", #' cliprect = c(200, 5, 400, 300)) #' #' # Using CSS selectors to pick out regions #' webshot("http://rstudio.github.io/leaflet", "leaflet-menu.png", selector = ".list-group") #' webshot("http://reddit.com/", "reddit-top.png", #' selector = c("input[type='text']", "#header-bottom-left")) #' #' # Expand selection region #' webshot("http://rstudio.github.io/leaflet", "leaflet-boxes.png", #' selector = "#installation", expand = c(10, 50, 0, 50)) #' #' # If multiple matches for a given selector, it uses the first match #' webshot("http://rstudio.github.io/leaflet", "leaflet-p.png", selector = "p") #' webshot("https://github.com/rstudio/shiny/", "shiny-stats.png", #' selector = "ul.numbers-summary") #' #' # Send commands to eval #' webshot("http://www.reddit.com/", "reddit-input.png", #' selector = c("#search", "#login_login-main"), #' eval = "casper.then(function() { #' // Check the remember me box #' this.click('#rem-login-main'); #' // Enter username and password #' this.sendKeys('#login_login-main input[type=\"text\"]', 'my_username'); #' this.sendKeys('#login_login-main input[type=\"password\"]', 'password'); #' #' // Now click in the search box. This results in a box expanding below #' this.click('#search input[type=\"text\"]'); #' // Wait 500ms #' this.wait(500); #' });" #' ) #' #' # Result can be piped to other commands like resize() and shrink() #' webshot("https://www.r-project.org/", "r-small.png") %>% #' resize("75%") %>% #' shrink() #' #' # Requests can change the User-Agent header #' webshot( #' "https://www.rstudio.com/products/rstudio/download/", #' "rstudio.png", #' useragent = "Mozilla/5.0 (Macintosh; Intel Mac OS X)" #' ) #' #' # See more examples in the package vignette # vignette("intro", package = "webshot") #' } #' #' @seealso \code{\link{appshot}} for taking screenshots of Shiny applications. #' @export webshot <- function( url = NULL, file = "webshot.png", vwidth = 992, vheight = 744, cliprect = NULL, selector = NULL, expand = NULL, delay = 0.2, zoom = 1, eval = NULL, debug = FALSE, useragent = NULL ) { if (is.null(url)) { stop("Need url.") } # Convert params cliprect, selector and expand to list if necessary if(!is.null(cliprect) && !is.list(cliprect)) cliprect <- list(cliprect) if(!is.null(selector) && !is.list(selector)) selector <- list(selector) if(!is.null(expand) && !is.list(expand)) expand <- list(expand) # Check length of arguments arg_list <- list( url = url, file = file, vwidth = vwidth, vheight = vheight, cliprect = cliprect, selector = selector, expand = expand, delay = delay, zoom = zoom, eval = eval, debug = debug, options = options ) arg_length <- vapply(arg_list, length, numeric(1)) max_arg_length <- max(arg_length) if (any(! arg_length %in% c(0, 1, max_arg_length))) { stop("All arguments should have same length or be single elements or NULL") } # If url is of length one replicate it to match the maximal length of arguments if (length(url) < max_arg_length) url <- rep(url, max_arg_length) # If user provides only one file name but wants several screenshots, then the # below code generates as many file names as URLs following the pattern # "filename001.png", "filename002.png", ... (or whatever extension it is) if (length(url) > 1 && length(file) == 1) { file <- vapply(1:length(url), FUN.VALUE = character(1), function(i) { replacement <- sprintf("%03d.\\1", i) gsub("\\.(.{3,4})$", replacement, file) }) } if (is_windows()) { url <- fix_windows_url(url) } if (!is.null(cliprect) && !is.null(selector)) { stop("Can't specify both cliprect and selector.") } else if (is.null(selector) && !is.null(cliprect)) { cliprect <- lapply(cliprect, function(x) { if (is.character(x)) { if (x == "viewport") { x <- c(0, 0, vwidth, vheight) } else { stop("Invalid value for cliprect: ", x) } } else { if (!is.numeric(x) || length(x) != 4) { stop("'cliprect' must be a 4-element numeric vector or a list of such vectors") } } x }) } # check that expand is a vector of length 1 or 4 or a list of such vectors if (!is.null(expand)) { lengths <- vapply(expand, length, numeric(1)) if (any(!lengths %in% c(1, 4))) { stop("'expand' must be a vector with one or four numbers, or a list of such vectors.") } } # Create the table that contains all options for each screenshot optsList <- data.frame(url = url, file = file, vwidth = vwidth, vheight = vheight) # Params selector, cliprect and expand can be either a vector that need to be # concatenated or a list of such vectors. This function can be used to convert # them into a character vector with the desired format. argToVec <- function(arg) { vapply(arg, FUN.VALUE = character(1), function(x) { if (any(is.null(x)) || any(is.na(x))) NA_character_ else paste(x, collapse = ",") }) } if (!is.null(cliprect)) optsList$cliprect <- argToVec(cliprect) if (!is.null(selector)) optsList$selector <- argToVec(selector) if (!is.null(expand)) optsList$expand <- argToVec(expand) if (!is.null(delay)) optsList$delay <- delay if (!is.null(zoom)) optsList$zoom <- zoom if (!is.null(eval)) optsList$eval <- eval if (!is.null(useragent)) optsList$options <- jsonlite::toJSON( list(pageSettings = list(userAgent = useragent)), auto_unbox = TRUE ) optsList$debug <- debug args <- list( # Workaround for SSL problem: https://github.com/wch/webshot/issues/51 # https://stackoverflow.com/questions/22461345/casperjs-status-fail-on-a-webpage "--ignore-ssl-errors=true", system.file("webshot.js", package = "webshot"), jsonlite::toJSON(optsList) ) res <- phantom_run(args) # Handle missing phantomjs if (is.null(res)) return(NULL) if (res != 0) { stop("webshot.js returned failure value: ", res) } structure(file, class = "webshot") } knit_print.webshot <- function(x, ...) { lapply(x, function(filename) { res <- readBin(filename, "raw", file.size(filename)) ext <- gsub(".*[.]", "", basename(filename)) structure(list(image = res, extension = ext), class = "html_screenshot") }) } #' @export print.webshot <- function(x, ...) { invisible(x) } webshot/R/pipe.R0000644000176200001440000000006713030514600013201 0ustar liggesusers#' @importFrom magrittr %>% #' @export magrittr::`%>%` webshot/R/appshot.R0000644000176200001440000001053313353470230013731 0ustar liggesusers#' Take a screenshot of a Shiny app #' #' \code{appshot} performs a \code{\link{webshot}} using two different methods #' depending upon the object provided. If a 'character' is provided (pointing to #' an app.R file or app directory) an isolated background R process is launched #' to run the Shiny application. The current R process then captures the #' \code{\link{webshot}}. When a Shiny application object is supplied to #' \code{appshot}, it is reversed: the Shiny application runs in the current R #' process and an isolated background R process is launched to capture a #' \code{\link{webshot}}. The reason it is reversed in the second case has to do #' with scoping: although it would be preferable to run the Shiny application in #' a background process and call \code{webshot} from the current process, with #' Shiny application objects, there are potential scoping errors when run this #' way. #' #' @inheritParams webshot #' @param app A Shiny app object, or a string naming an app directory. #' @param port Port that Shiny will listen on. #' @param envvars A named character vector or named list of environment #' variables and values to set for the Shiny app's R process. These will be #' unset after the process exits. This can be used to pass configuration #' information to a Shiny app. #' @param webshot_timeout The maximum number of seconds the phantom application #' is allowed to run before killing the process. If a delay argument is #' supplied (in \code{...}), the delay value is added to the timeout value. #' #' @param ... Other arguments to pass on to \code{\link{webshot}}. #' #' @rdname appshot #' @examples #' if (interactive()) { #' appdir <- system.file("examples", "01_hello", package="shiny") #' #' # With a Shiny directory #' appshot(appdir, "01_hello.png") #' #' # With a Shiny App object #' shinyapp <- shiny::shinyAppDir(appdir) #' appshot(shinyapp, "01_hello_app.png") #' } #' #' @export appshot <- function(app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL) { UseMethod("appshot") } #' @rdname appshot #' @export appshot.character <- function( app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL ) { port <- available_port(port) url <- shiny_url(port) # Run app in background with envvars p <- r_background_process( function(...) { shiny::runApp(...) }, args = list( appDir = app, port = port, display.mode = "normal", launch.browser = FALSE ), envvars = envvars ) on.exit({ p$kill() }) # Wait for app to start wait_until_server_exists(url) # Get screenshot fileout <- webshot(url, file = file, ...) invisible(fileout) } #' @rdname appshot #' @export appshot.shiny.appobj <- function( app, file = "webshot.png", ..., port = getOption("shiny.port"), envvars = NULL, webshot_timeout = 60 ) { port <- available_port(port) url <- shiny_url(port) args <- list( url = url, file = file, ..., timeout = webshot_app_timeout() ) p <- r_background_process( function(url, file, ..., timeout) { # Wait for app to start wait <- utils::getFromNamespace("wait_until_server_exists", "webshot") wait(url, timeout = timeout) webshot::webshot(url = url, file = file, ...) }, args, envvars = envvars ) on.exit({ p$kill() }) # add a delay to the webshot_timeout if it exists if(!is.null(args$delay)) { webshot_timeout <- webshot_timeout + args$delay } start_time <- as.numeric(Sys.time()) # Add a shiny app observer which checks every 200ms to see if the background r session is alive shiny::observe({ # check the r session rather than the file to avoid race cases or random issues if (p$is_alive()) { if ((as.numeric(Sys.time()) - start_time) <= webshot_timeout) { # try again later shiny::invalidateLater(200) } else { # timeout has occured. close the app and R session message("webshot timed out") p$kill() shiny::stopApp() } } else { # r_bg session has stopped, close the app shiny::stopApp() } return() }) # run the app shiny::runApp(app, port = port, display.mode = "normal", launch.browser = FALSE) # return webshot::webshot file value invisible(p$get_result()) # safe to call as the r_bg must have ended } webshot/R/process.R0000644000176200001440000000024213353465150013732 0ustar liggesusers r_background_process <- function(..., envvars = NULL) { if (is.null(envvars)) { envvars <- callr::rcmd_safe_env() } callr::r_bg(..., env = envvars) } webshot/NEWS.md0000644000176200001440000000712014446173504013033 0ustar liggesuserswebshot 0.5.5 ============= * Fixed #101, #116: When some parameters (like `cliprect` and `expand`) were used, it would raise `Warning: 'length(x) = 4 > 1' in coercion to 'logical(1)'`. In R 4.3, this warning changed to an error. (#117) webshot 0.5.4 ============= * Fixed #112: handling of Windows paths of the form "c:\file\path.html" did not work correctly. ([#114](https://github.com/wch/webshot/pull/114)) webshot 0.5.3 ============= * Fixed logic in `install_phantomjs()` when `force=TRUE` is used. ([#89](https://github.com/wch/webshot/pull/89)) * Fixed handling of `file://` URLs in Windows, when the URL contains a drive letter and colon, such as `"file://localhost/C:\\msys64"`. (#110, Thanks to Tomas Kalibera) webshot 0.5.2 ============= * Modified `install_phantomjs()` function to only install a new version of PhantomJS if the installed version is out of date or it isn't installed. (@coatless, [#82](https://github.com/wch/webshot/pull/82)) * Added `force` parameter. When it is set to `TRUE`, `install_phantomjs()` will reinstall phantomjs. (@coatless, [#82](https://github.com/wch/webshot/pull/82)) * Added `is_phantomjs_installed()` function to check if PhantomJS was installed on the user's computer. (@coatless, [#82](https://github.com/wch/webshot/pull/82)) * Fixed `phantom_paths()` function to detect the path to PhantomJS on Linux (@coatless, @wildintellect, [#85](https://github.com/wch/webshot/pull/85)) webshot 0.5.1 ============= * Added `debug` parameter. When it is set to `TRUE`, `webshot()` will print out debugging messages from PhantomJS and CasperJS. * Fixed [#51](https://github.com/wch/webshot/issues/51): Webshot had trouble with some sites that use HTTPS. * Added `appshot.shiny.appobj` functionality (schloerke, [#55](https://github.com/wch/webshot/pull/55)) webshot 0.5.0 ============= * Added support for R Markdown documents. ([#48](https://github.com/wch/webshot/pull/48)) * Closed [#42](https://github.com/wch/webshot/issues/42): Converted some instances of `system2()` to use processx instead. webshot 0.4.2 ============= * Fixed [#43](https://github.com/wch/webshot/issues/43): The `eval` argument for `webshot()` did not work. webshot 0.4.1 ============= * Updated vignette so it doesn't error when PhantomJS is not present. webshot 0.4.0 ============= * `webshot`, `resize`, and `shrink` all now accept a vector of URLs or filenames. (([#32](https://github.com/wch/webshot/pull/32)), [#33](https://github.com/wch/webshot/pull/33)) * Updated to CasperJS 1.1.3. * Added `zoom` option for higher-resolution screen shots. ([#26](https://github.com/wch/webshot/issues/26)) * `webshot()` now returns objects with class `webshot`. There is also a new `knit_print` method for `webshot` objects. ([#27](https://github.com/wch/webshot/pull/27)) * Fixed problem installing PhantomJS on R 3.3.2 and above. ([#35](https://github.com/wch/webshot/pull/35)) webshot 0.3.2 ============= * Better handling of local paths in Windows. ([#23](https://github.com/wch/webshot/issues/23)) * More robust searching for ImageMagick. ([#13](https://github.com/wch/webshot/issues/13)) webshot 0.3.1 ============= * The leading tilde in the path of PhantomJS is expanded now ([#19](https://github.com/wch/webshot/issues/19)). * Changed URL for PhantomJS binaries so that `install_phantomjs()` doesn't hit rate limits, and added workaround for downloading problems with R 3.3.0 and 3.3.1. webshot 0.3 =========== * The first CRAN release. Provided functions `webshot()`/`appshot()` to take screenshots via PhantomJS, and `resize()`/`shrink()` to manipulate images via GraphicsMagick/ImageMagick and OptiPNG. webshot/MD50000644000176200001440000000447314446417572012263 0ustar liggesusersebb3275a4d8c2f4c9c950731297c08d8 *DESCRIPTION cee5c3d1c2d36c835832ebe39c019a93 *NAMESPACE b84cb3634c0ec950357331664547a832 *NEWS.md dc6d26d0e55407ff7a9f4c4d38b14b21 *R/appshot.R a251b2655b27acd514a97ba7e81d030e *R/image.R 9aab2581ff22019c5db573e06686f2e1 *R/pipe.R d61de066985c86072a8787787f57bcb1 *R/process.R 343518cc64a49f4be75fcd13c3edd588 *R/rmdshot.R b24e2d4800f900a9d7553d4d3269ef8f *R/utils.R 6c1c9ae227ef4510183b1d04c6334a5d *R/wait.R 4cc191c08b558d487143df7fd0aa95d8 *R/webshot.R 0ec7fbcd065104b0422fafdc554338b6 *R/zzz.R b88cfc6c77760842ccb020abc93dea47 *README.md 7fa170eaadaa58b90accf3b78e113fe4 *inst/NOTICE a640c5cbbfd61cb3a9d7d185ca7d03f2 *inst/casperjs/bin/bootstrap.js de1e952c0a5cb9571d8fac141e2e20a7 *inst/casperjs/modules/casper.js 5ab42801f42396658323a8e23bf88aeb *inst/casperjs/modules/cli.js fc9b3f5b405ff5c20618567d7f96a39b *inst/casperjs/modules/clientutils.js fadcd2b801683c9da1209b173c97f097 *inst/casperjs/modules/colorizer.js 6d2c437ed10ba265de97049213a8a67d *inst/casperjs/modules/events.js f8ed4980298fc4dcf1972afca216465b *inst/casperjs/modules/http.js fa7e82fef1fd524866e05f5bb08ec220 *inst/casperjs/modules/mouse.js 0d15f5557ccd96ce9d4800cee441e26c *inst/casperjs/modules/pagestack.js d5f4801e2c674d613a7f8a51f789192f *inst/casperjs/modules/querystring.js 9a923dd4d1bc6a8ab945a8153c313d15 *inst/casperjs/modules/tester.js 03c1437cbeb34a470d9e9e0ed8499b7c *inst/casperjs/modules/utils.js e80238bf3b08224189b02ad8f72e5510 *inst/casperjs/modules/xunit.js 1d4297e212c4effa54a27a49f7a83664 *inst/casperjs/package.json 645925550ec2bbb80df7873063b79d5f *inst/examples/shiny.Rmd 812b510d5b867989bb579f28f84034f1 *inst/utils.js a17e8668b2e030897bde9777df05675e *inst/webshot.js bcf5066a2c95d3e5ad1f09a15449cdd7 *man/appshot.Rd 1cb62e11bd491eea3a15b7a4cef1c572 *man/figures/r-small-resized.png 418bb3fa1d5c2030b2082fc2341e10a3 *man/figures/r-small-zoomed.png be3742ef170e7d6bd6991924391ace1e *man/install_phantomjs.Rd bbbd69005bbc236770883a3c09265626 *man/is_phantomjs_installed.Rd 58a17f407550f9610c92389f0fd32e64 *man/reexports.Rd 828d38bfd37b3502800cb65f4cb64049 *man/resize.Rd 8b925de2a878473ee27eb87f4601b082 *man/rmdshot.Rd a3484b93ef9510a786ea3187ce298162 *man/shrink.Rd 2d3cc85a3e0461798a35e1c290759a72 *man/webshot.Rd 5b196372c93af922ad9c49dfbaf3cf9d *tests/testthat.R fb54b15d6794e3be8dbec219a8632928 *tests/testthat/test-url.R webshot/inst/0000755000176200001440000000000013353465150012707 5ustar liggesuserswebshot/inst/examples/0000755000176200001440000000000013353465150014525 5ustar liggesuserswebshot/inst/examples/shiny.Rmd0000644000176200001440000000423513353465150016327 0ustar liggesusers--- title: "Shiny Example" author: "RStudio" date: "2/28/2018" output: html_document runtime: shiny --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE) ``` This R Markdown document is made interactive using Shiny. Unlike the more traditional workflow of creating static reports, you can now create documents that allow your readers to change the assumptions underlying your analysis and see the results immediately. To learn more, see [Interactive Documents](http://rmarkdown.rstudio.com/authoring_shiny.html). ## Inputs and Outputs You can embed Shiny inputs and outputs in your document. Outputs are automatically updated whenever inputs change. This demonstrates how a standard R plot can be made interactive by wrapping it in the Shiny `renderPlot` function. The `selectInput` and `sliderInput` functions create the input widgets used to drive the plot. ```{r eruptions, echo=FALSE} inputPanel( selectInput("n_breaks", label = "Number of bins:", choices = c(10, 20, 35, 50), selected = 20), sliderInput("bw_adjust", label = "Bandwidth adjustment:", min = 0.2, max = 2, value = 1, step = 0.2) ) renderPlot({ hist(faithful$eruptions, probability = TRUE, breaks = as.numeric(input$n_breaks), xlab = "Duration (minutes)", main = "Geyser eruption duration") dens <- density(faithful$eruptions, adjust = input$bw_adjust) lines(dens, col = "blue") }) ``` ## Embedded Application It's also possible to embed an entire Shiny application within an R Markdown document using the `shinyAppDir` function. This example embeds a Shiny application located in another directory: ```{r tabsets, echo=FALSE} shinyAppDir( system.file("examples/06_tabsets", package = "shiny"), options = list( width = "100%", height = 550 ) ) ``` Note the use of the `height` parameter to determine how much vertical space the embedded application should occupy. You can also use the `shinyApp` function to define an application inline rather then in an external directory. In all of R code chunks above the `echo = FALSE` attribute is used. This is to prevent the R code within the chunk from rendering in the document alongside the Shiny components. webshot/inst/webshot.js0000644000176200001440000001307713353465150014730 0ustar liggesusers// This must be executed with phantomjs // Take a screenshot of a URL and saves it to a .png file // phantomjs webshot.js // // 'optsList' is a JSON array containing configurations for each screenshot that has // to be taken. Each configuration object needs to contain at least properties // "url" and "file". For instance: // [{"url":"http://rstudio.github.io/leaflet/","file":"webshot.png"}] var utils = require('./utils'); var system = require('system'); phantom.casperPath = phantom.libraryPath + '/casperjs'; phantom.injectJs(phantom.casperPath + '/bin/bootstrap.js'); var opt_defaults = { delay: 0.2, vwidth: 992, vheight: 744, zoom: 1 }; // ===================================================================== // Command line arguments // ===================================================================== var args = system.args; if (args.length < 2) { console.log( 'Usage:\n' + ' phantomjs webshot.js \n' + '\n' + 'optsList is a JSON array containing configuration for each screenshot.\n' + 'For instance:\n' + '\'[{"url":"url1.html","file":"file1.png"},{"url":"url2.html","file":"fil2.png","zoom":2}]\''); phantom.exit(1); } var optsList = JSON.parse(args[1]); // Options passed to CasperJS var casperOpts = {}; if (optsList[0].options) { casperOpts = JSON.parse(optsList[0].options); } // `debug` is a special option. The value from the first element in the // optsList array is applied globally. if (optsList[0].debug) { casperOpts.verbose = true; casperOpts.logLevel = 'debug'; } delete optsList.debug; var casper = require('casper').create(casperOpts); // ===================================================================== // Screenshot // ===================================================================== casper.start(); casper.options.onLoadError = function(c, url) { console.log("Could not load ", url); phantom.exit(1); }; casper.eachThen(optsList, function(response) { var opts = response.data; // Prepare options opts = utils.fillMissing(opts, opt_defaults); // This should be four numbers separated by "," if (opts.cliprect) { opts.cliprect = opts.cliprect.split(","); opts.cliprect = opts.cliprect.map(function(x) { return +x; }); } // Can be 1 or 4 numbers separated by "," if (opts.expand) { opts.expand = opts.expand.split(","); opts.expand = opts.expand.map(function(x) { return +x; }); if (opts.expand.length !== 1 && opts.expand.length !== 4) { console.log("'expand' must have either 1 or 4 values."); phantom.exit(1); } } // Can have multiple selectors if (opts.selector) { opts.selector = opts.selector.split(","); } // Go to url and perform the desired screenshot this.zoom(opts.zoom) .viewport(opts.zoom * opts.vwidth, opts.zoom * opts.vheight) .thenOpen(opts.url) .wait(opts.delay * 1000) .then(function() { if (opts.eval) { eval(opts.eval); } }) .then(function() { var cr = findClipRect(opts, this); this.capture(opts.file, cr); }); }); casper.run(); // ===================================================================== // Utility functions // ===================================================================== // Given the options object, return an object representing the clipping // rectangle. If opts.cliprect and opts.selector are both not present, // return null. function findClipRect(opts, casper) { // Convert top,right,bottom,left object to top,left,width,height function rel2abs(r) { return { top: r.top, left: r.left, bottom: r.top + r.height, right: r.left + r.width }; } // Convert top,left,width,height object to top,right,bottom,left function abs2rel(r) { return { top: r.top, left: r.left, width: r.right - r.left, height: r.bottom - r.top }; } var rect; if (opts.cliprect) { rect = { top: opts.cliprect[0] * opts.zoom, left: opts.cliprect[1] * opts.zoom, width: opts.cliprect[2] * opts.zoom, height: opts.cliprect[3] * opts.zoom }; } else if (opts.selector) { var selector = opts.selector; // Get bounds, in absolute coordinates so that we can find a bounding // rectangle around multiple items. var bounds = selector.map(function(s) { var b = casper.getElementBounds(s); return rel2abs(b); }); // Get bounding rectangle around multiple bounds = bounds.reduce(function(a, b) { return { top: Math.min(a.top, b.top), left: Math.min(a.left, b.left), bottom: Math.max(a.bottom, b.bottom), right: Math.max(a.right, b.right) }; }); // Convert back to width + height format rect = abs2rel(bounds); } else { return null; } // Expand clipping rectangle if (opts.expand) { var expand = opts.expand; if (expand.length === 1) { expand = [expand[0], expand[0], expand[0], expand[0]]; } rect = rel2abs(rect); rect.top -= expand[0] * opts.zoom; rect.right += expand[1] * opts.zoom; rect.bottom += expand[2] * opts.zoom; rect.left -= expand[3] * opts.zoom; rect = abs2rel(rect); } return rect; } // Exit on error, instead of just hanging phantom.onError = function(msg, trace) { var msgStack = ['PHANTOM ERROR: ' + msg]; if (trace && trace.length) { msgStack.push('TRACE:'); trace.forEach(function(t) { msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); }); } console.log(msgStack.join('\n')); phantom.exit(1); }; webshot/inst/NOTICE0000644000176200001440000000222013030514600013572 0ustar liggesusersThe casperjs/ directory contains components from CasperJS (http://casperjs.org), which is MIT-licensed: Copyright (c) 2011-2015 Nicolas Perriault Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. webshot/inst/casperjs/0000755000176200001440000000000013030514600014504 5ustar liggesuserswebshot/inst/casperjs/modules/0000755000176200001440000000000013030514600016154 5ustar liggesuserswebshot/inst/casperjs/modules/tester.js0000644000176200001440000017072113030514600020030 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ var require = patchRequire(require); var fs = require('fs'); var events = require('events'); var utils = require('utils'); var f = utils.format; function AssertionError(msg, result) { "use strict"; Error.call(this); this.message = msg; this.name = 'AssertionError'; this.result = result; } AssertionError.prototype = new Error(); exports.AssertionError = AssertionError; function TerminationError(msg) { "use strict"; Error.call(this); this.message = msg; this.name = 'TerminationError'; } TerminationError.prototype = new Error(); exports.TerminationError = TerminationError; function TimedOutError(msg) { "use strict"; Error.call(this); this.message = msg; this.name = 'TimedOutError'; } TimedOutError.prototype = new Error(); exports.TimedOutError = TimedOutError; /** * Creates a tester instance. * * @param Casper casper A Casper instance * @param Object options Tester options * @return Tester */ exports.create = function create(casper, options) { "use strict"; return new Tester(casper, options); }; /** * Casper tester: makes assertions, stores test results and display then. * * @param Casper casper A valid Casper instance * @param Object|null options Options object */ var Tester = function Tester(casper, options) { "use strict"; /*eslint max-statements:0*/ if (!utils.isCasperObject(casper)) { throw new CasperError("Tester needs a Casper instance"); } // self reference var self = this; // casper reference this.casper = casper; // public properties this._setUp = undefined; this._tearDown = undefined; this.aborted = false; this.executed = 0; this.currentTestFile = null; this.currentTestStartTime = new Date(); this.currentSuite = undefined; this.currentSuiteNum = 0; this.lastAssertTime = 0; this.loadIncludes = { includes: [], pre: [], post: [] }; this.options = utils.mergeObjects({ concise: false, // concise output? failFast: false, // terminates a suite as soon as a test fails? failText: "FAIL", // text to use for a failed test passText: "PASS", // text to use for a succesful test skipText: "SKIP", // text to use for a skipped test pad: 80 , // maximum number of chars for a result line warnText: "WARN" // text to use for a dubious test }, options); this.queue = []; this.running = false; this.started = false; this.suiteResults = new TestSuiteResult(); this.on('success', function onSuccess(success) { var timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addSuccess(success, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; }); this.on('skipped', function onSkipped(skipped) { var timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addSkip(skipped, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; }); this.on('fail', function onFail(failure) { // export var valueKeys = Object.keys(failure.values), timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addFailure(failure, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; // special printing if (failure.type) { this.comment(' type: ' + failure.type); } if (failure.file) { this.comment(' file: ' + failure.file + (failure.line ? ':' + failure.line : '')); } if (failure.lineContents) { this.comment(' code: ' + failure.lineContents); } if (!failure.values || valueKeys.length === 0) { return; } valueKeys.forEach(function(name) { this.comment(f(' %s: %s', name, utils.formatTestValue(failure.values[name], name))); }.bind(this)); // check for fast failing if (this.options.failFast) { return this.terminate('--fail-fast: aborted all remaining tests'); } }); function errorHandler(error, backtrace) { self.casper.unwait(); if (error instanceof Error) { self.processError(error); return; } if (utils.isString(error) && /^(Assertion|Termination|TimedOut)Error/.test(error)) { return; } var line = 0; try { line = (backtrace || []).filter(function(entry) { return self.currentTestFile === entry.file; })[0].line; } catch (e) {} self.uncaughtError(error, self.currentTestFile, line, backtrace); } function errorHandlerAndDone(error, backtrace) { errorHandler(error, backtrace); self.done(); } // casper events this.casper.on('error', function onCasperError(msg, backtrace) { self.processPhantomError(msg, backtrace); }); [ 'wait.error', 'waitFor.timeout.error', 'event.error', 'complete.error' ].forEach(function(event) { self.casper.on(event, errorHandlerAndDone); }); self.casper.on('step.error', errorHandler); this.casper.on('warn', function(warning) { if (self.currentSuite) { self.currentSuite.addWarning(warning); } }); // Do not hook casper if we're not testing if (!phantom.casperTest) { return; } // specific timeout callbacks this.casper.options.onStepTimeout = function test_onStepTimeout(timeout, step) { throw new TimedOutError(f("Step timeout occured at step %s (%dms)", step, timeout)); }; this.casper.options.onTimeout = function test_onTimeout(timeout) { throw new TimedOutError(f("Timeout occured (%dms)", timeout)); }; this.casper.options.onWaitTimeout = function test_onWaitTimeout(timeout, details) { /*eslint complexity:0*/ var message = f("Wait timeout occured (%dms)", timeout); details = details || {}; if (details.selector) { message = f(details.waitWhile ? '"%s" never went away in %dms' : '"%s" still did not exist in %dms', details.selector, timeout); } else if (details.visible) { message = f(details.waitWhile ? '"%s" never disappeared in %dms' : '"%s" never appeared in %dms', details.visible, timeout); } else if (details.url || details.resource) { message = f('%s did not load in %dms', details.url || details.resource, timeout); } else if (details.popup) { message = f('%s did not pop up in %dms', details.popup, timeout); } else if (details.text) { message = f('"%s" did not appear in the page in %dms', details.text, timeout); } else if (details.selectorTextChange) { message = f('"%s" did not have a text change in %dms', details.selectorTextChange, timeout); } else if (utils.isFunction(details.testFx)) { message = f('"%s" did not evaluate to something truthy in %dms', details.testFx.toString(), timeout); } errorHandlerAndDone(new TimedOutError(message)); }; }; // Tester class is an EventEmitter utils.inherits(Tester, events.EventEmitter); exports.Tester = Tester; /** * Aborts current test suite. * * @param String message Warning message (optional) */ Tester.prototype.abort = function abort(message) { "use strict"; throw new TerminationError(message || 'test suite aborted'); }; /** * Skip `nb` tests. * * @param Integer nb Number of tests to skip * @param String message Message to display * @return Object */ Tester.prototype.skip = function skip(nb, message) { "use strict"; return this.processAssertionResult({ success: null, standard: f("%d test%s skipped", nb, nb > 1 ? "s" : ""), message: message, type: "skip", number: nb, skipped: true }); }; /** * Skip `nb` test on specific engine(s). * * A skip specifier is an object of the form: * { * name: 'casperjs' | 'phantomjs', * version: { * min: Object, * max: Object * }, * message: String * } * * Minimal and maximal versions to be skipped are determined using * utils.matchEngine. * * @param Integer nb Number of tests to skip * @param Mixed skipSpec a single skip specifier object or * an Array of skip specifier objects * @return Object */ Tester.prototype.skipIfEngine = function skipIfEngine(nb, skipSpec) { skipSpec = utils.matchEngine(skipSpec); if (skipSpec) { var message = skipSpec.name; var version = skipSpec.version; var skipMessage = skipSpec.message; if (version) { var min = version.min; var max = version.max; if (min && min === max) { message += ' ' + min; } else { if (min) { message += ' from ' + min; } if (max) { message += ' to ' + max; } } } if (skipMessage) { message += ' ' + skipMessage; } return this.skip(nb, message); } return false; }; /** * Asserts that a condition strictly resolves to true. Also returns an * "assertion object" containing useful informations about the test case * results. * * This method is also used as the base one used for all other `assert*` * family methods; supplementary informations are then passed using the * `context` argument. * * Note: an AssertionError is thrown if the assertion fails. * * @param Boolean subject The condition to test * @param String message Test description * @param Object|null context Assertion context object (Optional) * @return Object An assertion result object if test passed * @throws AssertionError in case the test failed */ Tester.prototype.assert = Tester.prototype.assertTrue = function assert(subject, message, context) { "use strict"; this.executed++; var result = utils.mergeObjects({ success: subject === true, type: "assert", standard: "Subject is strictly true", message: message, file: this.currentTestFile, doThrow: true, values: { subject: utils.getPropertyPath(context, 'values.subject') || subject } }, context || {}); if (!result.success && result.doThrow) { throw new AssertionError(message || result.standard, result); } return this.processAssertionResult(result); }; /** * Asserts that two values are strictly equals. * * @param Mixed subject The value to test * @param Mixed expected The expected value * @param String message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertEquals = Tester.prototype.assertEqual = function assertEquals(subject, expected, message) { "use strict"; return this.assert(utils.equals(subject, expected), message, { type: "assertEquals", standard: "Subject equals the expected value", values: { subject: subject, expected: expected } }); }; /** * Asserts that two values are strictly not equals. * * @param Mixed subject The value to test * @param Mixed expected The unwanted value * @param String|null message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertNotEquals = function assertNotEquals(subject, shouldnt, message) { "use strict"; return this.assert(!this.testEquals(subject, shouldnt), message, { type: "assertNotEquals", standard: "Subject doesn't equal what it shouldn't be", values: { subject: subject, shouldnt: shouldnt } }); }; /** * Asserts that a selector expression matches n elements. * * @param Mixed selector A selector expression * @param Number count Expected number of matching elements * @param String message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertElementCount = function assertElementCount(selector, count, message) { "use strict"; if (!utils.isNumber(count) || count < 0) { throw new CasperError('assertElementCount() needs a positive integer count'); } var elementCount = this.casper.evaluate(function(selector) { try { return __utils__.findAll(selector).length; } catch (e) { return -1; } }, selector); return this.assert(elementCount === count, message, { type: "assertElementCount", standard: f('%d element%s matching selector "%s" found', count, count > 1 ? 's' : '', selector), values: { selector: selector, expected: count, obtained: elementCount } }); }; /** * Asserts that a code evaluation in remote DOM resolves to true. * * @param Function fn A function to be evaluated in remote DOM * @param String message Test description * @param Object params Object/Array containing the parameters to inject into * the function (optional) * @return Object An assertion result object */ Tester.prototype.assertEval = Tester.prototype.assertEvaluate = function assertEval(fn, message, params) { "use strict"; return this.assert(this.casper.evaluate(fn, params), message, { type: "assertEval", standard: "Evaluated function returns true", values: { fn: fn, params: params } }); }; /** * Asserts that the result of a code evaluation in remote DOM equals * an expected value. * * @param Function fn The function to be evaluated in remote DOM * @param Boolean expected The expected value * @param String|null message Test description * @param Object|null params Object containing the parameters to inject into the * function (optional) * @return Object An assertion result object */ Tester.prototype.assertEvalEquals = Tester.prototype.assertEvalEqual = function assertEvalEquals(fn, expected, message, params) { "use strict"; var subject = this.casper.evaluate(fn, params); return this.assert(utils.equals(subject, expected), message, { type: "assertEvalEquals", standard: "Evaluated function returns the expected value", values: { fn: fn, params: params, subject: subject, expected: expected } }); }; function baseFieldAssert(inputName, expected, actual, message) { "use strict"; return this.assert(utils.equals(actual, expected), message, { type: 'assertField', standard: f('"%s" input field has the value "%s"', inputName, expected), values: { inputName: inputName, actual: actual, expected: expected } }); } /** * Asserts that the provided assertion fails (used for internal testing). * * @param Function fn A closure calling an assertion * @param String|null message Test description * @return Object An assertion result object */ Tester.prototype.assertFail = function assertFail(fn, message) { "use strict"; var failed = false; try { fn(); } catch (e) { failed = true; } return this.assert(failed, message, { type: "assertFail", standard: "Assertion fails as expected" }); }; /** * Asserts that a given input field has the provided value. * * @param String|Object input The name attribute of the input element * or an object with the selector * @param String expected The expected value of the input element * @param String message Test description * @param Object options ClientUtils#getFieldValue options (optional) * @return Object An assertion result object */ Tester.prototype.assertField = function assertField(input, expected, message, options) { "use strict"; if (typeof input === 'object') { switch (input.type) { case 'css': return this.assertFieldCSS(input.path, expected, message); case 'xpath': return this.assertFieldXPath(input.path, expected, message); default: throw new CasperError('Invalid regexp.'); // no default } } var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'name')); }, input); return baseFieldAssert.call(this, input, expected, actual, message); }; /** * Asserts that a given input field by CSS selector has the provided value. * * @param Object cssSelector The CSS selector to use for the assert field value * @param String expected The expected value of the input element * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFieldCSS = function assertFieldCSS(cssSelector, expected, message) { "use strict"; var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'css')); }, cssSelector); return baseFieldAssert.call(this, null, expected, actual, message); }; /** * Asserts that a given input field by XPath selector has the provided value. * * @param Object xPathSelector The XPath selector to use for the assert field value * @param String expected The expected value of the input element * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFieldXPath = function assertFieldXPath(xPathSelector, expected, message) { "use strict"; var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'xpath')); }, xPathSelector); return baseFieldAssert.call(this, null, expected, actual, message); }; /** * Asserts that an element matching the provided selector expression exists in * remote DOM. * * @param String selector Selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertExists = Tester.prototype.assertExist = Tester.prototype.assertSelectorExists = Tester.prototype.assertSelectorExist = function assertExists(selector, message) { "use strict"; return this.assert(this.casper.exists(selector), message, { type: "assertExists", standard: f("Find an element matching: %s", selector), values: { selector: selector } }); }; /** * Asserts that an element matching the provided selector expression does not * exist in remote DOM. * * @param String selector Selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertDoesntExist = Tester.prototype.assertNotExists = function assertDoesntExist(selector, message) { "use strict"; return this.assert(!this.casper.exists(selector), message, { type: "assertDoesntExist", standard: f("Fail to find element matching selector: %s", selector), values: { selector: selector } }); }; /** * Asserts that current HTTP status is the one passed as argument. * * @param Number status HTTP status code * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertHttpStatus = function assertHttpStatus(status, message) { "use strict"; var currentHTTPStatus = this.casper.currentHTTPStatus; return this.assert(utils.equals(this.casper.currentHTTPStatus, status), message, { type: "assertHttpStatus", standard: f("HTTP status code is: %s", status), values: { current: currentHTTPStatus, expected: status } }); }; /** * Asserts that a provided string matches a provided RegExp pattern. * * @param String subject The string to test * @param RegExp pattern A RegExp object instance * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertMatch = Tester.prototype.assertMatches = function assertMatch(subject, pattern, message) { "use strict"; if (utils.betterTypeOf(pattern) !== "regexp") { throw new CasperError('Invalid regexp.'); } return this.assert(pattern.test(subject), message, { type: "assertMatch", standard: "Subject matches the provided pattern", values: { subject: subject, pattern: pattern.toString() } }); }; /** * Asserts a condition resolves to false. * * @param Boolean condition The condition to test * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertNot = Tester.prototype.assertFalse = function assertNot(condition, message) { "use strict"; return this.assert(!condition, message, { type: "assertNot", standard: "Subject is falsy", values: { condition: condition } }); }; /** * Asserts that a selector expression is not currently visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertNotVisible = Tester.prototype.assertInvisible = function assertNotVisible(selector, message) { "use strict"; return this.assert(!this.casper.visible(selector), message, { type: "assertNotVisible", standard: "Selector is not visible", values: { selector: selector } }); }; /** * Asserts that the provided function called with the given parameters * will raise an exception. * * @param Function fn The function to test * @param Array args The arguments to pass to the function * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertRaises = Tester.prototype.assertRaise = Tester.prototype.assertThrows = function assertRaises(fn, args, message) { "use strict"; var error, thrown = false, context = { type: "assertRaises", standard: "Function raises an error" }; try { fn.apply(null, args); } catch (err) { thrown = true; error = err; } this.assert(thrown, message, utils.mergeObjects(context, { values: { error: error } })); }; /** * Asserts that the current page has a resource that matches the provided test * * @param Function/String test A test function that is called with every response * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertResourceExists = Tester.prototype.assertResourceExist = function assertResourceExists(test, message) { "use strict"; return this.assert(this.casper.resourceExists(test), message, { type: "assertResourceExists", standard: "Confirm page has resource", values: { test: test } }); }; /** * Asserts that given text doesn't exist in the document body. * * @param String text Text not to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTextDoesntExist = Tester.prototype.assertTextDoesntExist = function assertTextDoesntExist(text, message) { "use strict"; var textFound = (this.casper.evaluate(function _evaluate() { return document.body.textContent || document.body.innerText; }).indexOf(text) === -1); return this.assert(textFound, message, { type: "assertTextDoesntExists", standard: "Text doesn't exist within the document body", values: { text: text } }); }; /** * Asserts that given text exists in the document body. * * @param String text Text to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTextExists = Tester.prototype.assertTextExist = function assertTextExists(text, message) { "use strict"; var textFound = (this.casper.evaluate(function _evaluate() { return document.body.textContent || document.body.innerText; }).indexOf(text) !== -1); return this.assert(textFound, message, { type: "assertTextExists", standard: "Find text within the document body", values: { text: text } }); }; /** * Asserts a subject is truthy. * * @param Mixed subject Test subject * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTruthy = function assertTruthy(subject, message) { "use strict"; /*eslint eqeqeq:0*/ return this.assert(utils.isTruthy(subject), message, { type: "assertTruthy", standard: "Subject is truthy", values: { subject: subject } }); }; /** * Asserts a subject is falsy. * * @param Mixed subject Test subject * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFalsy = function assertFalsy(subject, message) { "use strict"; /*eslint eqeqeq:0*/ return this.assert(utils.isFalsy(subject), message, { type: "assertFalsy", standard: "Subject is falsy", values: { subject: subject } }); }; /** * Asserts that given text exists in the provided selector. * * @param String selector Selector expression * @param String text Text to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertSelectorHasText = Tester.prototype.assertSelectorContains = function assertSelectorHasText(selector, text, message) { "use strict"; var got = this.casper.fetchText(selector); var textFound = got.indexOf(text) !== -1; return this.assert(textFound, message, { type: "assertSelectorHasText", standard: f('Find "%s" within the selector "%s"', text, selector), values: { selector: selector, text: text, actualContent: got } }); }; /** * Asserts that given text does not exist in the provided selector. * * @param String selector Selector expression * @param String text Text not to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertSelectorDoesntHaveText = Tester.prototype.assertSelectorDoesntContain = function assertSelectorDoesntHaveText(selector, text, message) { "use strict"; var textFound = this.casper.fetchText(selector).indexOf(text) === -1; return this.assert(textFound, message, { type: "assertSelectorDoesntHaveText", standard: f('Did not find "%s" within the selector "%s"', text, selector), values: { selector: selector, text: text } }); }; /** * Asserts that title of the remote page equals to the expected one. * * @param String expected The expected title string * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTitle = function assertTitle(expected, message) { "use strict"; var currentTitle = this.casper.getTitle(); return this.assert(utils.equals(currentTitle, expected), message, { type: "assertTitle", standard: f('Page title is: "%s"', expected), values: { subject: currentTitle, expected: expected } }); }; /** * Asserts that title of the remote page matched the provided pattern. * * @param RegExp pattern The pattern to test the title against * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTitleMatch = Tester.prototype.assertTitleMatches = function assertTitleMatch(pattern, message) { "use strict"; if (utils.betterTypeOf(pattern) !== "regexp") { throw new CasperError('Invalid regexp.'); } var currentTitle = this.casper.getTitle(); return this.assert(pattern.test(currentTitle), message, { type: "assertTitle", details: "Page title does not match the provided pattern", values: { subject: currentTitle, pattern: pattern.toString() } }); }; /** * Asserts that the provided subject is of the given type. * * @param mixed subject The value to test * @param String type The javascript type name * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertType = function assertType(subject, type, message) { "use strict"; var actual = utils.betterTypeOf(subject); return this.assert(utils.equals(actual, type), message, { type: "assertType", standard: f('Subject type is: "%s"', type), values: { subject: subject, type: type, actual: actual } }); }; /** * Asserts that the provided subject has the provided constructor in its prototype hierarchy. * * @param mixed subject The value to test * @param Function constructor The javascript type name * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertInstanceOf = function assertInstanceOf(subject, constructor, message) { "use strict"; if (utils.betterTypeOf(constructor) !== "function") { throw new CasperError('Subject is null or undefined.'); } return this.assert(utils.betterInstanceOf(subject, constructor), message, { type: "assertInstanceOf", standard: f('Subject is instance of: "%s"', constructor.name), values: { subject: subject, constructorName: constructor.name } }); }; /** * Asserts that a the current page url matches a given pattern. A pattern may be * either a RegExp object or a String. The method will test if the URL matches * the pattern or contains the String. * * @param RegExp|String pattern The test pattern * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertUrlMatch = Tester.prototype.assertUrlMatches = function assertUrlMatch(pattern, message) { "use strict"; var currentUrl = this.casper.getCurrentUrl(), patternType = utils.betterTypeOf(pattern), result; if (patternType === "regexp") { result = pattern.test(currentUrl); } else if (patternType === "string") { result = currentUrl.indexOf(pattern) !== -1; } else { throw new CasperError("assertUrlMatch() only accepts strings or regexps"); } return this.assert(result, message, { type: "assertUrlMatch", standard: "Current url matches the provided pattern", values: { currentUrl: currentUrl, pattern: pattern.toString() } }); }; /** * Asserts that a selector expression is currently visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertVisible = function assertVisible(selector, message) { "use strict"; return this.assert(this.casper.visible(selector), message, { type: "assertVisible", standard: "Selector is visible", values: { selector: selector } }); }; /** * Asserts that all elements matching selector expression are currently visible. * Fails if even one element is not visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertAllVisible = function assertAllVisible(selector, message) { "use strict"; return this.assert(this.casper.allVisible(selector), message, { type: "assertAllVisible", standard: "All elements matching selector are visible", values: { selector: selector } }); }; /** * Prints out a colored bar onto the console. * */ Tester.prototype.bar = function bar(text, style) { "use strict"; this.casper.echo(text, style, this.options.pad); }; /** * Defines a function which will be executed before every test. * * @param Function fn */ Tester.prototype.setUp = function setUp(fn) { "use strict"; this._setUp = fn; }; /** * Defines a function which will be executed after every test. * * @param Function fn */ Tester.prototype.tearDown = function tearDown(fn) { "use strict"; this._tearDown = fn; }; /** * Starts a suite. * * Can be invoked different ways: * * casper.test.begin("suite description", plannedTests, function(test){}) * casper.test.begin("suite description", function(test){}) */ Tester.prototype.begin = function begin() { "use strict"; if (this.started && this.running) return this.queue.push(arguments); function getConfig(args) { var config = { setUp: function(){}, tearDown: function(){} }; if (utils.isFunction(args[1])) { config.test = args[1]; } else if (utils.isObject(args[1])) { config = utils.mergeObjects(config, args[1]); } else if (utils.isNumber(args[1]) && utils.isFunction(args[2])) { config.planned = ~~args[1] || undefined; config.test = args[2]; } else if (utils.isNumber(args[1]) && utils.isObject(args[2])) { config.config = utils.mergeObjects(config, args[2]); config.planned = ~~args[1] || undefined; } else { throw new CasperError('Invalid call'); } if (!utils.isFunction(config.test)) throw new CasperError('begin() is missing a mandatory test function'); return config; } var description = arguments[0] || f("Untitled suite in %s", this.currentTestFile), config = getConfig([].slice.call(arguments)), next = function() { config.test(this, this.casper); if (!this.options.concise) { this.casper.echo([ this.colorize('PASS', 'INFO'), this.formatMessage(description), this.colorize(f('(%d test%s)', config.planned, config.planned > 1 ? 's' : ''), 'INFO') ].join(' ')); } }.bind(this); if (!this.options.concise) this.comment(description); this.currentSuite = new TestCaseResult({ name: description, file: this.currentTestFile, config: config, planned: config.planned || undefined }); this.executed = 0; this.running = this.started = true; try { if (config.setUp) config.setUp(this, this.casper); if (!this._setUp) return next(); if (this._setUp.length > 0) return this._setUp.call(this, next); // async this._setUp.call(this); // sync next(); } catch (err) { this.processError(err); this.done(); } }; /** * Render a colorized output. Basically a proxy method for * `Casper.Colorizer#colorize()`. * * @param String message * @param String style The style name * @return String */ Tester.prototype.colorize = function colorize(message, style) { "use strict"; return this.casper.getColorizer().colorize(message, style); }; /** * Writes a comment-style formatted message to stdout. * * @param String message */ Tester.prototype.comment = function comment(message) { "use strict"; this.casper.echo('# ' + message, 'COMMENT'); }; /** * Declares the current test suite done. * */ Tester.prototype.done = function done() { "use strict"; /*eslint max-statements:0, complexity:0*/ var planned, config = this.currentSuite && this.currentSuite.config || {}; if (arguments.length && utils.isNumber(arguments[0])) { this.casper.warn('done() `planned` arg is deprecated as of 1.1'); planned = arguments[0]; } if (config && config.tearDown && utils.isFunction(config.tearDown)) { try { config.tearDown(this, this.casper); } catch (error) { this.processError(error); } } var next = function() { if (this.currentSuite && this.currentSuite.planned && this.currentSuite.planned !== this.executed + this.currentSuite.skipped && !this.currentSuite.failed) { this.dubious(this.currentSuite.planned, this.executed, this.currentSuite.name); } else if (planned && planned !== this.executed) { // BC this.dubious(planned, this.executed); } if (this.currentSuite) { this.suiteResults.push(this.currentSuite); this.currentSuite = undefined; this.executed = 0; } this.emit('test.done'); this.casper.currentHTTPResponse = {}; this.running = this.started = false; var nextTest = this.queue.shift(); if (nextTest) { this.begin.apply(this, nextTest); } }.bind(this); if (!this._tearDown) { return next(); } try { if (this._tearDown.length > 0) { // async this._tearDown.call(this, next); } else { // sync this._tearDown.call(this); next(); } } catch (error) { this.processError(error); } }; /** * Marks a test as dubious, when the number of planned tests doesn't match the * number of actually executed one. * * @param String message */ Tester.prototype.dubious = function dubious(planned, executed, suite) { "use strict"; var message = f('%s: %d tests planned, %d tests executed', suite || 'global', planned, executed); this.casper.warn(message); if (!this.currentSuite) return; this.currentSuite.addFailure({ type: "dubious", file: this.currentTestFile, standard: message }); }; /** * Writes an error-style formatted message to stdout. * * @param String message */ Tester.prototype.error = function error(message) { "use strict"; this.casper.echo(message, 'ERROR'); }; /** * Executes a file, wraping and evaluating its code in an isolated * environment where only the current `casper` instance is passed. * * @param String file Absolute path to some js/coffee file */ Tester.prototype.exec = function exec(file) { "use strict"; file = this.filter('exec.file', file) || file; if (!fs.isFile(file) || !utils.isJsFile(file)) { var e = new CasperError(f("Cannot exec %s: can only exec() files with .js or .coffee extensions", file)); e.fileName = e.file = e.sourceURL = file; throw e; } this.currentTestFile = file; phantom.injectJs(file); }; /** * Adds a failed test entry to the stack. * * @param String message * @param Object Failure context (optional) */ Tester.prototype.fail = function fail(message, context) { "use strict"; context = context || {}; return this.assert(false, message, utils.mergeObjects({ type: "fail", standard: "explicit call to fail()" }, context)); }; /** * Recursively finds all test files contained in a given directory. * * @param String dir Path to some directory to scan */ Tester.prototype.findTestFiles = function findTestFiles(dir) { "use strict"; var self = this; if (!fs.isDirectory(dir)) { return []; } var entries = fs.list(dir).filter(function _filter(entry) { return entry !== '.' && entry !== '..'; }).map(function _map(entry) { return fs.absolute(fs.pathJoin(dir, entry)); }); entries.forEach(function _forEach(entry) { if (fs.isDirectory(entry)) { entries = entries.concat(self.findTestFiles(entry)); } }); return entries.filter(function _filter(entry) { return utils.isJsFile(entry); }).sort(); }; /** * Computes current suite identifier. * * @return String */ Tester.prototype.getCurrentSuiteId = function getCurrentSuiteId() { "use strict"; return this.casper.test.currentSuiteNum + "-" + this.casper.step; }; /** * Formats a message to highlight some parts of it. * * @param String message * @param String style */ Tester.prototype.formatMessage = function formatMessage(message, style) { "use strict"; var parts = /^([a-z0-9_\.]+\(\))(.*)/i.exec(message); if (!parts) { return message; } return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style); }; /** * Writes an info-style formatted message to stdout. * * @param String message */ Tester.prototype.info = function info(message) { "use strict"; this.casper.echo(message, 'PARAMETER'); }; /** * Adds a succesful test entry to the stack. * * @param String message */ Tester.prototype.pass = function pass(message) { "use strict"; return this.assert(true, message, { type: "pass", standard: "explicit call to pass()" }); }; function getStackEntry(error, testFile) { "use strict"; if ("stackArray" in error) { // PhantomJS has changed the API of the Error object :-/ // https://github.com/ariya/phantomjs/commit/c9cf14f221f58a3daf585c47313da6fced0276bc return error.stackArray.filter(function(entry) { return testFile === entry.sourceURL; })[0]; } if (! ('stack' in error)) return null; var r = /\r?\n\s*(.*?)(at |@)([^:]*?):(\d+):?(\d*)/g; var m; while ((m = r.exec(error.stack))) { var sourceURL = m[3]; if (sourceURL.indexOf('->') !== -1) { sourceURL = sourceURL.split('->')[1].trim(); } if (sourceURL === testFile) { return { sourceURL: sourceURL, line: m[4], column: m[5]}; } } return null; } /** * Processes an assertion error. * * @param AssertionError error */ Tester.prototype.processAssertionError = function(error) { "use strict"; var result = error && error.result || {}, testFile = this.currentTestFile, stackEntry; try { stackEntry = getStackEntry(error, testFile); } catch (e) {} if (stackEntry) { result.line = stackEntry.line; try { result.lineContents = fs.read(this.currentTestFile).split('\n')[result.line - 1].trim(); } catch (e) {} } return this.processAssertionResult(result); }; /** * Processes an assertion result by emitting the appropriate event and * printing result onto the console. * * @param Object result An assertion result object * @return Object The passed assertion result Object */ Tester.prototype.processAssertionResult = function processAssertionResult(result) { "use strict"; if (!this.currentSuite) { // this is for BC when begin() didn't exist this.currentSuite = new TestCaseResult({ name: "Untitled suite in " + this.currentTestFile, file: this.currentTestFile, planned: undefined }); } var eventName = 'success', message = result.message || result.standard, style = 'INFO', status = this.options.passText; if (null === result.success) { eventName = 'skipped'; style = 'SKIP'; status = this.options.skipText; } else if (!result.success) { eventName = 'fail'; style = 'RED_BAR'; status = this.options.failText; } if (!this.options.concise) { this.casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' ')); } this.emit(eventName, result); return result; }; /** * Processes an error. * * @param Error error */ Tester.prototype.processError = function processError(error) { "use strict"; if (error instanceof AssertionError) { return this.processAssertionError(error); } if (error instanceof TerminationError) { return this.terminate(error.message); } return this.uncaughtError(error, this.currentTestFile, error.line); }; /** * Processes a PhantomJS error, which is an error message and a backtrace. * * @param String message * @param Array backtrace */ Tester.prototype.processPhantomError = function processPhantomError(msg, backtrace) { "use strict"; if (/^AssertionError/.test(msg)) { this.casper.warn('looks like you did not use begin(), which is mandatory since 1.1'); } var termination = /^TerminationError:?\s?(.*)/.exec(msg); if (termination) { var message = termination[1]; if (backtrace && backtrace[0]) { message += ' at ' + backtrace[0].file + backtrace[0].line; } return this.terminate(message); } this.fail(msg, { type: "error", doThrow: false, values: { error: msg, stack: backtrace } }); this.done(); }; /** * Renders a detailed report for each failed test. * */ Tester.prototype.renderFailureDetails = function renderFailureDetails() { "use strict"; if (!this.suiteResults.isFailed()) { return; } var failures = this.suiteResults.getAllFailures(); this.casper.echo(f("\nDetails for the %d failed test%s:\n", failures.length, failures.length > 1 ? "s" : ""), "PARAMETER"); failures.forEach(function _forEach(failure) { this.casper.echo(f('In %s%s', failure.file, ~~failure.line ? ':' + ~~failure.line : '')); if (failure.suite) { this.casper.echo(f(' %s', failure.suite), "PARAMETER"); } this.casper.echo(f(' %s: %s', failure.type || "unknown", failure.message || failure.standard || "(no message was entered)"), "COMMENT"); }.bind(this)); }; /** * Render tests results, an optionally exit phantomjs. * * @param Boolean exit Exit casper after results have been rendered? * @param Number status Exit status code (default: 0) * @param String save Optional path to file where to save the results log */ Tester.prototype.renderResults = function renderResults(exit, status, save) { "use strict"; /*eslint max-statements:0*/ save = save || this.options.save; var exitStatus = 0, failed = this.suiteResults.countFailed(), total = this.suiteResults.countExecuted(), statusText, style, result; if (total === 0) { exitStatus = 1; statusText = this.options.warnText; style = 'WARN_BAR'; result = f("%s Looks like you didn't run any tests.", statusText); } else { if (this.suiteResults.isFailed()) { exitStatus = 1; statusText = this.options.failText; style = 'RED_BAR'; } else { statusText = this.options.passText; style = 'GREEN_BAR'; } result = f('%s %d test%s executed in %ss, %d passed, %d failed, %d dubious, %d skipped.', statusText, total, total > 1 ? "s" : "", utils.ms2seconds(this.suiteResults.calculateDuration()), this.suiteResults.countPassed(), failed, this.suiteResults.countDubious(), this.suiteResults.countSkipped()); } this.casper.echo(result, style, this.options.pad); this.renderFailureDetails(); if (save) { this.saveResults(save); } if (exit === true) { this.emit("exit"); this.casper.exit(status ? ~~status : exitStatus); } }; /** * Runs all suites contained in the paths passed as arguments. * */ Tester.prototype.runSuites = function runSuites() { "use strict"; var testFiles = [], self = this; if (arguments.length === 0) { throw new CasperError("runSuites() needs at least one path argument"); } this.loadIncludes.includes.forEach(function _forEachInclude(include) { phantom.injectJs(include); }); this.loadIncludes.pre.forEach(function _forEachPreTest(preTestFile) { testFiles = testFiles.concat(preTestFile); }); Array.prototype.forEach.call(arguments, function _forEachArgument(path) { if (!fs.exists(path)) { self.bar(f("Path %s doesn't exist", path), "RED_BAR"); } if (fs.isDirectory(path)) { testFiles = testFiles.concat(self.findTestFiles(path)); } else if (fs.isFile(path)) { testFiles.push(path); } }); this.loadIncludes.post.forEach(function _forEachPostTest(postTestFile) { testFiles = testFiles.concat(postTestFile); }); if (testFiles.length === 0) { this.bar(f("No test file found in %s, terminating.", Array.prototype.slice.call(arguments)), "RED_BAR"); this.casper.exit(1); } self.currentSuiteNum = 0; self.currentTestStartTime = new Date(); self.lastAssertTime = 0; var interval = setInterval(function _check(self) { if (self.running) { return; } if (self.currentSuiteNum === testFiles.length || self.aborted) { self.emit('tests.complete'); clearInterval(interval); self.aborted = false; } else { self.runTest(testFiles[self.currentSuiteNum]); self.currentSuiteNum++; } }, 20, this); }; /** * Runs a test file * */ Tester.prototype.runTest = function runTest(testFile) { "use strict"; this.bar(f('Test file: %s', testFile), 'INFO_BAR'); this.running = true; // this.running is set back to false with done() this.executed = 0; this.exec(testFile); }; /** * Terminates current suite. * */ Tester.prototype.terminate = function(message) { "use strict"; if (message) { this.casper.warn(message); } this.done(); this.aborted = true; this.emit('tests.complete'); }; /** * Saves results to file. * * @param String filename Target file path. */ Tester.prototype.saveResults = function saveResults(filepath) { "use strict"; var exporter = require('xunit').create(); exporter.setResults(this.suiteResults); try { fs.write(filepath, exporter.getSerializedXML(), 'w'); this.casper.echo(f('Result log stored in %s', filepath), 'INFO', 80); } catch (e) { this.casper.echo(f('Unable to write results to %s: %s', filepath, e), 'ERROR', 80); } }; /** * Tests equality between the two passed arguments. * * @param Mixed v1 * @param Mixed v2 * @param Boolean */ Tester.prototype.testEquals = Tester.prototype.testEqual = function testEquals(v1, v2) { "use strict"; return utils.equals(v1, v2); }; /** * Processes an error caught while running tests contained in a given test * file. * * @param Error|String error The error * @param String file Test file where the error occurred * @param Number line Line number (optional) * @param Array backtrace Error stack trace (optional) */ Tester.prototype.uncaughtError = function uncaughtError(error, file, line, backtrace) { "use strict"; // XXX: this is NOT an assertion scratch that return this.processAssertionResult({ success: false, type: "uncaughtError", file: file, line: ~~line, message: utils.isObject(error) ? error.message : error, values: { error: error, stack: backtrace } }); }; /** * Test suites array. * */ function TestSuiteResult() {} TestSuiteResult.prototype = []; exports.TestSuiteResult = TestSuiteResult; /** * Returns the number of tests. * * @return Number */ TestSuiteResult.prototype.countTotal = function countTotal() { "use strict"; return this.countPassed() + this.countFailed() + this.countDubious(); }; /** * Returns the number of dubious results. * * @return Number */ TestSuiteResult.prototype.countDubious = function countDubious() { "use strict"; return this.map(function(result) { return result.dubious; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of executed tests. * * @return Number */ TestSuiteResult.prototype.countExecuted = function countTotal() { "use strict"; return this.countTotal() - this.countDubious(); }; /** * Returns the number of errors. * * @return Number */ TestSuiteResult.prototype.countErrors = function countErrors() { "use strict"; return this.map(function(result) { return result.crashed; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of failed tests. * * @return Number */ TestSuiteResult.prototype.countFailed = function countFailed() { "use strict"; return this.map(function(result) { return result.failed - result.dubious; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of succesful tests. * * @return Number */ TestSuiteResult.prototype.countPassed = function countPassed() { "use strict"; return this.map(function(result) { return result.passed; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of skipped tests. * * @return Number */ TestSuiteResult.prototype.countSkipped = function countSkipped() { "use strict"; return this.map(function(result) { return result.skipped; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of warnings. * * @return Number */ TestSuiteResult.prototype.countWarnings = function countWarnings() { "use strict"; return this.map(function(result) { return result.warned; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Checks if the suite has failed. * * @return Number */ TestSuiteResult.prototype.isFailed = function isFailed() { "use strict"; return this.countErrors() + this.countFailed() + this.countDubious() > 0; }; /** * Checks if the suite has skipped tests. * * @return Number */ TestSuiteResult.prototype.isSkipped = function isSkipped() { "use strict"; return this.countSkipped() > 0; }; /** * Returns all failures from this suite. * * @return Array */ TestSuiteResult.prototype.getAllFailures = function getAllFailures() { "use strict"; var failures = []; this.forEach(function(result) { failures = failures.concat(result.failures); }); return failures; }; /** * Returns all succesful tests from this suite. * * @return Array */ TestSuiteResult.prototype.getAllPasses = function getAllPasses() { "use strict"; var passes = []; this.forEach(function(result) { passes = passes.concat(result.passes); }); return passes; }; /** * Returns all skipped tests from this suite. * * @return Array */ TestSuiteResult.prototype.getAllSkips = function getAllSkips() { "use strict"; var skipped = []; this.forEach(function(result) { skipped = skipped.concat(result.skipped); }); return skipped; }; /** * Returns all results from this suite. * * @return Array */ TestSuiteResult.prototype.getAllResults = function getAllResults() { "use strict"; return this.getAllPasses().concat(this.getAllFailures()); }; /** * Computes the sum of all durations of the tests which were executed in the * current suite. * * @return Number */ TestSuiteResult.prototype.calculateDuration = function calculateDuration() { "use strict"; return this.getAllResults().map(function(result) { return ~~result.time; }).reduce(function add(a, b) { return a + b; }, 0); }; /** * Test suite results object. * * @param Object options */ function TestCaseResult(options) { "use strict"; this.name = options && options.name; this.file = options && options.file; this.planned = ~~(options && options.planned) || undefined; this.errors = []; this.failures = []; this.passes = []; this.skips = []; this.warnings = []; this.config = options && options.config; this.__defineGetter__("assertions", function() { return this.passed + this.failed; }); this.__defineGetter__("crashed", function() { return this.errors.length; }); this.__defineGetter__("failed", function() { return this.failures.length; }); this.__defineGetter__("dubious", function() { return this.failures.filter(function(failure) { return failure.type === "dubious"; }).length; }); this.__defineGetter__("passed", function() { return this.passes.length; }); this.__defineGetter__("skipped", function() { return this.skips.map(function(skip) { return skip.number; }).reduce(function(a, b) { return a + b; }, 0); }); } exports.TestCaseResult = TestCaseResult; /** * Adds a failure record and its execution time. * * @param Object failure * @param Number time */ TestCaseResult.prototype.addFailure = function addFailure(failure, time) { "use strict"; failure.suite = this.name; failure.time = time; this.failures.push(failure); }; /** * Adds an error record. * * @param Object failure */ TestCaseResult.prototype.addError = function addFailure(error) { "use strict"; error.suite = this.name; this.errors.push(error); }; /** * Adds a success record and its execution time. * * @param Object success * @param Number time */ TestCaseResult.prototype.addSuccess = function addSuccess(success, time) { "use strict"; success.suite = this.name; success.time = time; this.passes.push(success); }; /** * Adds a success record and its execution time. * * @param Object success * @param Number time */ TestCaseResult.prototype.addSkip = function addSkip(skipped, time) { "use strict"; skipped.suite = this.name; skipped.time = time; this.skips.push(skipped); }; /** * Adds a warning message. * NOTE: quite contrary to addError, addSuccess, and addSkip * this adds a String value, NOT an Object * * @param String warning */ TestCaseResult.prototype.addWarning = function addWarning(warning) { "use strict"; this.warnings.push(warning); }; /** * Computes total duration for this suite. * * @return Number */ TestCaseResult.prototype.calculateDuration = function calculateDuration() { "use strict"; function add(a, b) { return a + b; } var passedTimes = this.passes.map(function(success) { return ~~success.time; }).reduce(add, 0); var failedTimes = this.failures.map(function(failure) { return ~~failure.time; }).reduce(add, 0); return passedTimes + failedTimes; }; webshot/inst/casperjs/modules/colorizer.js0000644000176200001440000001126113030514600020523 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global exports, console, patchRequire, require:true*/ var require = patchRequire(require); var fs = require('fs'); var utils = require('utils'); var env = require('system').env; exports.create = function create(type) { "use strict"; if (!type) { return; } if (!(type in exports)) { throw new Error(utils.format('Unsupported colorizer type "%s"', type)); } return new exports[type](); }; /** * This is a port of lime colorizer. * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php * * (c) Fabien Potencier, Symfony project, MIT license */ var Colorizer = function Colorizer() { "use strict"; /*eslint no-multi-spaces:0*/ var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 }; var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 }; var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 }; var styles = { 'ERROR': { bg: 'red', fg: 'white', bold: true }, 'INFO': { fg: 'green', bold: true }, 'TRACE': { fg: 'green', bold: true }, 'PARAMETER': { fg: 'cyan' }, 'COMMENT': { fg: 'yellow' }, 'WARNING': { fg: 'red', bold: true }, 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true }, 'RED_BAR': { fg: 'white', bg: 'red', bold: true }, 'INFO_BAR': { bg: 'cyan', fg: 'white', bold: true }, 'WARN_BAR': { bg: 'yellow', fg: 'white', bold: true }, 'SKIP': { fg: 'magenta', bold: true }, 'SKIP_BAR': { bg: 'magenta', fg: 'white', bold: true } }; /** * Adds a style to provided text. * * @param String text * @param String styleName * @return String */ this.colorize = function colorize(text, styleName, pad) { if ((fs.isWindows() && (!env['ANSICON'] && env['ConEmuANSI'] !== 'ON')) || !(styleName in styles)) { return text; } return this.format(text, styles[styleName], pad); }; /** * Formats a text using a style declaration object. * * @param String text * @param Object style * @return String */ this.format = function format(text, style, pad) { if ((fs.isWindows() && (!env['ANSICON'] && env['ConEmuANSI'] !== 'ON')) || !utils.isObject(style)) { return text; } var codes = []; if (style.fg && foreground[style.fg]) { codes.push(foreground[style.fg]); } if (style.bg && background[style.bg]) { codes.push(background[style.bg]); } for (var option in options) { if (option in style && style[option] === true) { codes.push(options[option]); } } // pad if (typeof pad === "number" && text.length < pad) { text += new Array(pad - text.length + 1).join(' '); } return "\u001b[" + codes.join(';') + 'm' + text + "\u001b[0m"; }; }; exports.Colorizer = Colorizer; /** * Dummy colorizer. Does basically nothing. * */ var Dummy = function Dummy() { "use strict"; this.colorize = function colorize(text, styleName, pad) { return text; }; this.format = function format(text, style, pad){ return text; }; }; exports.Dummy = Dummy; webshot/inst/casperjs/modules/http.js0000644000176200001440000000461113030514600017473 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ var require = patchRequire(require); var utils = require('utils'); /* * Building an Array subclass */ function responseHeaders(){} responseHeaders.prototype = []; /** * Retrieves a given header based on its name * * @param String name A case-insensitive response header name * @return mixed A header string or `null` if not found */ responseHeaders.prototype.get = function get(name){ "use strict"; var headerValue = null; name = name.toLowerCase(); this.some(function(header){ if (header.name.toLowerCase() === name){ headerValue = header.value; return true; } }); return headerValue; }; /** * Augments the response with proper prototypes. * * @param Mixed response Phantom response or undefined (generally with local files) * @return Object Augmented response */ exports.augmentResponse = function(response) { "use strict"; if (!utils.isHTTPResource(response)) { return; } response.headers.__proto__ = responseHeaders.prototype; return response; }; webshot/inst/casperjs/modules/xunit.js0000644000176200001440000001566513030514600017676 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global CasperError, console, exports, phantom, patchRequire, require:true*/ var require = patchRequire(require); var utils = require('utils'); var fs = require('fs'); var TestSuiteResult = require('tester').TestSuiteResult; /** * Generates a value for 'classname' attribute of the JUnit XML report. * * Uses the (relative) file name of the current casper script without file * extension as classname. * * @param String classname * @return String */ function generateClassName(classname) { "use strict"; classname = (classname || "").replace(phantom.casperPath, "").trim(); var script = classname || phantom.casperScript || ""; if (script.indexOf(fs.workingDirectory) === 0) { script = script.substring(fs.workingDirectory.length + 1); } if (script.indexOf('/') === 0) { script = script.substring(1, script.length); } if (~script.indexOf('.')) { script = script.substring(0, script.lastIndexOf('.')); } // If we have trimmed our string down to nothing, default to script name if (!script && phantom.casperScript) { script = phantom.casperScript; } return script || "unknown"; } /** * Creates a XUnit instance * * @return XUnit */ exports.create = function create() { "use strict"; return new XUnitExporter(); }; /** * JUnit XML (xUnit) exporter for test results. * */ function XUnitExporter() { "use strict"; this.setupDocument(); // Initialize state this.results = undefined; this.rendered = false; } exports.XUnitExporter = XUnitExporter; /** * Retrieves generated XML object - actually an HTMLElement. * * @return HTMLElement */ XUnitExporter.prototype.getXML = function getXML() { "use strict"; var self = this; if (!(this.results instanceof TestSuiteResult)) { throw new CasperError('Results not set, cannot get XML.'); } this.results.forEach(function(result) { var suiteNode = utils.node('testsuite', { name: result.name, tests: result.assertions, failures: result.failed, errors: result.crashed, time: utils.ms2seconds(result.calculateDuration()), timestamp: (new Date()).toISOString(), 'package': generateClassName(result.file) }); // successful test cases result.passes.forEach(function(success) { var testCase = utils.node('testcase', { name: success.message || success.standard, classname: generateClassName(success.file), time: utils.ms2seconds(~~success.time) }); suiteNode.appendChild(testCase); }); // failed test cases result.failures.forEach(function(failure) { var testCase = utils.node('testcase', { name: failure.name || failure.message || failure.standard, classname: generateClassName(failure.file), time: utils.ms2seconds(~~failure.time) }); var failureNode = utils.node('failure', { type: failure.type || "failure" }); failureNode.appendChild(self._xmlDocument.createCDATASection(failure.message || "no message left")); if (failure.values && failure.values.error instanceof Error) { var errorNode = utils.node('error', { type: utils.betterTypeOf(failure.values.error) }); errorNode.appendChild(self._xmlDocument.createCDATASection(failure.values.error.stack)); testCase.appendChild(errorNode); } testCase.appendChild(failureNode); suiteNode.appendChild(testCase); }); // errors result.errors.forEach(function(error) { var errorNode = utils.node('error', { type: error.name }); errorNode.appendChild(self._xmlDocument.createCDATASection(error.stack ? error.stack : error.message)); suiteNode.appendChild(errorNode); }); // warnings var warningNode = utils.node('system-out'); warningNode.appendChild(self._xmlDocument.createCDATASection(result.warnings.join('\n'))); suiteNode.appendChild(warningNode); this._xml.appendChild(suiteNode); }.bind(this)); this._xml.setAttribute('time', utils.ms2seconds(this.results.calculateDuration())); this.rendered = true; return this._xmlDocument; }; /** * Retrieves generated Xunit XML * * @return string */ XUnitExporter.prototype.getSerializedXML = function getSerializedXML() { "use strict"; var serializer = new XMLSerializer(), document; if ( !this.rendered ) { document = this.getXML(); } return '' + serializer.serializeToString(document); }; /** * Sets test results. * * @param TestSuite results */ XUnitExporter.prototype.setResults = function setResults(results) { "use strict"; if (!(results instanceof TestSuiteResult)) { throw new CasperError('Invalid results type.'); } this.results = results; // New results let's re-initialize this.setupDocument(); this.rendered = false; return results; }; /** * Initializes the XML to an empty document * * @return void */ XUnitExporter.prototype.setupDocument = function() { // Note that we do NOT use a documentType here, because validating // parsers try to fetch the (non-existing) DTD and fail #1528 this._xmlDocument = document.implementation.createDocument("", ""); this._xml = this._xmlDocument.appendChild(this._xmlDocument.createElement("testsuites")); }; webshot/inst/casperjs/modules/events.js0000644000176200001440000002261113030514600020020 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global CasperError*/ var isArray = Array.isArray; function EventEmitter() { this._filters = {}; } exports.EventEmitter = EventEmitter; // By default EventEmitters will print a warning if more than // 10 listeners are added to it. This is a useful default which // helps finding memory leaks. // // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. var defaultMaxListeners = 10; EventEmitter.prototype.setMaxListeners = function(n) { if (!this._events) this._events = {}; this._maxListeners = n; }; EventEmitter.prototype.emit = function emit() { var type = arguments[0]; // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events || !this._events.error || (isArray(this._events.error) && !this._events.error.length)) { if (arguments[1] instanceof Error) { throw arguments[1]; // Unhandled 'error' event } else { throw new CasperError("Uncaught, unspecified 'error' event."); } } } if (!this._events) return false; var handler = this._events[type]; if (!handler) return false; if (typeof handler === 'function') { try { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: var l = arguments.length; var args = new Array(l - 1); for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; handler.apply(this, args); } } catch (err) { this.emit('event.error', err); } return true; } else if (isArray(handler)) { var l = arguments.length; var args = new Array(l - 1); for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; var listeners = handler.slice(); for (var i = 0, l = listeners.length; i < l; i++) { listeners[i].apply(this, args); } return true; } else { return false; } }; // EventEmitter is defined in src/node_events.cc // EventEmitter.prototype.emit() is also defined there. EventEmitter.prototype.addListener = function addListener(type, listener) { if ('function' !== typeof listener) { throw new CasperError('addListener only takes instances of Function'); } if (!this._events) this._events = {}; // To avoid recursion in the case that type == "newListeners"! Before // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, listener); if (!this._events[type]) { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; } else if (isArray(this._events[type])) { // If we've already got an array, just append. this._events[type]['fail' === type ? 'unshift' : 'push'](listener); // Check for listener leak if (!this._events[type].warned) { var m; if (this._maxListeners !== undefined) { m = this._maxListeners; } else { m = defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); console.trace(); } } } else { // Adding the second element, need to change to array. this._events[type] = 'fail' === type ? [listener, this._events[type]] : [this._events[type], listener]; } return this; }; EventEmitter.prototype.prependListener = function prependListener(type, listener) { if ('function' !== typeof listener) { throw new CasperError('addListener only takes instances of Function'); } if (!this._events) this._events = {}; // To avoid recursion in the case that type == "newListeners"! Before // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, listener); if (!this._events[type]) { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; } else if (isArray(this._events[type])) { // If we've already got an array, just append. this._events[type].unshift(listener); // Check for listener leak if (!this._events[type].warned) { var m; if (this._maxListeners !== undefined) { m = this._maxListeners; } else { m = defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); console.trace(); } } } else { // Adding the second element, need to change to array. this._events[type] = [listener, this._events[type]]; } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function once(type, listener) { if ('function' !== typeof listener) { throw new CasperError('.once only takes instances of Function'); } var self = this; function g() { self.removeListener(type, g); listener.apply(this, arguments); } g.listener = listener; self.on(type, g); return this; }; EventEmitter.prototype.removeListener = function removeListener(type, listener) { if ('function' !== typeof listener) { throw new CasperError('removeListener only takes instances of Function'); } // does not use listeners(), so no side effect of creating _events[type] if (!this._events || !this._events[type]) return this; var list = this._events[type]; if (isArray(list)) { var position = -1; for (var i = 0, length = list.length; i < length; i++) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; list.splice(position, 1); if (list.length === 0) delete this._events[type]; } else if (list === listener || (list.listener && list.listener === listener)) { delete this._events[type]; } return this; }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { if (arguments.length === 0) { this._events = {}; return this; } // does not use listeners(), so no side effect of creating _events[type] if (type && this._events && this._events[type]) this._events[type] = null; return this; }; EventEmitter.prototype.listeners = function listeners(type) { if (!this._events) this._events = {}; if (!this._events[type]) this._events[type] = []; if (!isArray(this._events[type])) { this._events[type] = [this._events[type]]; } return this._events[type]; }; // Added for CasperJS: filters a value attached to an event EventEmitter.prototype.filter = function filter() { var type = arguments[0]; if (!this._filters) { this._filters = {}; return; } var _filter = this._filters[type]; if (typeof _filter !== 'function') { return; } return _filter.apply(this, Array.prototype.splice.call(arguments, 1)); }; EventEmitter.prototype.removeAllFilters = function removeAllFilters(type) { if (arguments.length === 0) { this._filters = {}; return this; } if (type && this._filters && this._filters[type]) { this._filters[type] = null; } return this; }; EventEmitter.prototype.setFilter = function setFilter(type, filterFn) { if (!this._filters) { this._filters = {}; } if ('function' !== typeof filterFn) { throw new CasperError('setFilter only takes instances of Function'); } if (!this._filters[type]) { this._filters[type] = filterFn; return true; } // TODO: process multiple filters? in which order? disallow? return false; }; webshot/inst/casperjs/modules/mouse.js0000644000176200001440000002120213030514600017637 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global CasperError, exports, patchRequire, require:true*/ var require = patchRequire(require); var utils = require('utils'); var Mouse = function Mouse(casper) { "use strict"; if (!utils.isCasperObject(casper)) { throw new CasperError('Mouse() needs a Casper instance'); } var slice = Array.prototype.slice, nativeEvents = ['mouseup', 'mousedown', 'click', 'mousemove'], nativeButtons = ['left', 'middle', 'right']; if (utils.gteVersion(phantom.version, '1.8.0')) { nativeEvents.push('doubleclick'); } if (utils.gteVersion(phantom.version, '2.1.0')) { nativeEvents.push('contextmenu'); } var emulatedEvents = ['mouseover', 'mouseout', 'mouseenter', 'mouseleave'], supportedEvents = nativeEvents.concat(emulatedEvents); var computeCenter = function computeCenter(selector) { var bounds = casper.getElementBounds(selector); if (utils.isClipRect(bounds)) { var x = Math.round(bounds.left + bounds.width / 2), y = Math.round(bounds.top + bounds.height / 2); return [x, y]; } return [0, 0]; }; var getPointFromViewPort = function getPointFromViewPort(page, x, y){ var px = x - x % page.viewportSize.width; var py = y - y % page.viewportSize.height; var max = casper.evaluate(function() { return [__utils__.getDocumentWidth(), __utils__.getDocumentHeight()]; }); if (py > max[0] - page.viewportSize.width && max[0] > page.viewportSize.width){ px = max[0] - page.viewportSize.width; } if (py > max[1] - page.viewportSize.height && max[1] > page.viewportSize.height){ py = max[1] - page.viewportSize.height; } page.scrollPosition = { 'left': px, 'top': py }; return [ x - px, y - py ]; }; var getPointFromSelectorCoords = function getPointFromSelectorCoords(selector, clientX, clientY){ var convertNumberToIntAndPercentToFloat = function convertNumberToIntAndPercentToFloat(a, def){ return !!a && !isNaN(a) && parseInt(a, 10) || !!a && !isNaN(parseFloat(a)) && parseFloat(a) >= 0 && parseFloat(a) <= 100 && parseFloat(a) / 100 || def; }; var bounds = casper.getElementBounds(selector), px = convertNumberToIntAndPercentToFloat(clientX, 0.5), py = convertNumberToIntAndPercentToFloat(clientY, 0.5); if (utils.isClipRect(bounds)) { return [ bounds.left + (px ^ 0) + Math.round(bounds.width * (px - (px ^ 0)).toFixed(10)), bounds.top + (py ^ 0) + Math.round(bounds.height * (py - (py ^ 0)).toFixed(10)) ]; } return [1, 1]; }; var processEvent = function processEvent(type, args) { var button = nativeButtons[0], selector = 'html', index = 0, point, scroll = casper.page.scrollPosition; if (!utils.isString(type) || supportedEvents.indexOf(type) === -1) { throw new CasperError('Mouse.processEvent(): Unsupported mouse event type: ' + type); } if (emulatedEvents.indexOf(type) > -1) { casper.log("Mouse.processEvent(): no native fallback for type " + type, "warning"); } args = [].slice.call(args); // cast Arguments -> Array if (args.length === 0) { throw new CasperError('Mouse.processEvent(): Too few arguments'); } if (isNaN(parseInt(args[0], 10)) && casper.exists(args[0])) { selector = args[0]; index++; } if (args.length >= index + 2) { point = getPointFromSelectorCoords(selector, args[index], args[index + 1]); } else { point = computeCenter(selector); } index = nativeButtons.indexOf(args[args.length - 1]); if (index > -1) { button = nativeButtons[index]; } casper.emit('mouse.' + type.replace('mouse', ''), args); point = getPointFromViewPort(casper.page, point[0], point[1]); casper.page.sendEvent.apply(casper.page, [type].concat(point).concat([button])); casper.page.scrollPosition = scroll; }; this.click = function click() { processEvent('click', arguments); }; this.doubleclick = function doubleclick() { processEvent('doubleclick', arguments); }; this.down = function down() { processEvent('mousedown', arguments); }; this.move = function move() { processEvent('mousemove', arguments); }; this.processEvent = function() { processEvent(arguments[0], [].slice.call(arguments, 1)); }; this.rightclick = function rightclick() { try { processEvent('contextmenu', arguments); } catch (e) { var args = slice.call(arguments); switch (args.length) { case 0: throw new CasperError('Mouse.rightclick(): Too few arguments'); case 1: casper.mouseEvent('contextmenu', args[0]); break; case 2: if (!utils.isNumber(args[0]) || !utils.isNumber(args[1])) { throw new CasperError('Mouse.rightclick(): No valid coordinates passed: ' + args); } var struct = casper.page.evaluate(function (clientX, clientY) { var xpath = function xpath(el) { if (typeof el === "string") { return document.evaluate(el, document, null, 0, null); } if (!el || el.nodeType !== 1) { return ''; } if (el.id) { return "//*[@id='" + el.id + "']"; } var sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName === el.tagName; }); return xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '[' + ([].indexOf.call(sames, el) + 1) + ']' : ''); }; try { var elem = document.elementFromPoint(clientX, clientY); var rec = elem.getBoundingClientRect(); return { "selector": {"type": "xpath", "path": xpath(elem)}, "relX": clientX - rec.left, "relY": clientY - rec.top }; } catch (ex) { return { "selector": {"type": "xpath", "path": "//html"}, "relX": clientX, "relY": clientY }; } }, args[0], args[1]); casper.mouseEvent('contextmenu', struct.selector, struct.relX, struct.relY); break; default: throw new CasperError('Mouse.rightclick(): Too many arguments'); } } }; this.up = function up() { processEvent('mouseup', arguments); }; }; exports.create = function create(casper) { "use strict"; return new Mouse(casper); }; exports.Mouse = Mouse; webshot/inst/casperjs/modules/pagestack.js0000644000176200001440000001124613030514600020460 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global CasperError, console, exports, phantom, patchRequire, require:true*/ var require = patchRequire(require); var utils = require('utils'); var f = utils.format; function create() { "use strict"; return new Stack(); } exports.create = create; /** * Popups container. Implements Array prototype. * */ var Stack = function Stack(){}; exports.Stack = Stack; Stack.prototype = []; /** * Cleans the stack from any closed popups. * * @return Number New stack length */ Stack.prototype.clean = function clean() { "use strict"; var self = this; this.forEach(function(popup, index) { // window references lose the parent attribute when they are no longer valid if (popup.parent === null || typeof popup.parent === "undefined") { self.splice(index, 1); } }); return this.length; }; /** * Finds a popup matching the provided information. Information can be: * * - RegExp: matching page url * - String: strict page url value * - WebPage: a direct WebPage instance * * @param Mixed popupInfo * @return WebPage */ Stack.prototype.find = function find(popupInfo) { "use strict"; var popup, type = utils.betterTypeOf(popupInfo); switch (type) { case "regexp": popup = this.findByRegExp(popupInfo); break; case "string": popup = this.findByURL(popupInfo); break; case "qtruntimeobject": // WebPage popup = popupInfo; if (!utils.isWebPage(popup) || !this.some(function(popupPage) { if (popupInfo.id && popupPage.id) { return popupPage.id === popup.id; } return popupPage.url === popup.url; })) { throw new CasperError("Invalid or missing popup."); } break; default: throw new CasperError(f("Invalid popupInfo type: %s.", type)); } return popup; }; /** * Finds the first popup which url matches a given RegExp. * * @param RegExp regexp * @return WebPage */ Stack.prototype.findByRegExp = function findByRegExp(regexp) { "use strict"; var popup = this.filter(function(popupPage) { return regexp.test(popupPage.url); })[0]; if (!popup) { throw new CasperError(f("Couldn't find popup with url matching pattern %s", regexp)); } return popup; }; /** * Finds the first popup matching a given url. * * @param String url The child WebPage url * @return WebPage */ Stack.prototype.findByURL = function findByURL(string) { "use strict"; var popup = this.filter(function(popupPage) { return popupPage.url.indexOf(string) !== -1; })[0]; if (!popup) { throw new CasperError(f("Couldn't find popup with url containing '%s'", string)); } return popup; }; /** * Returns a human readable list of current active popup urls. * * @return Array Mapped stack. */ Stack.prototype.list = function list() { "use strict"; return this.map(function(popup) { try { return popup.url; } catch (e) { return ''; } }); }; /** * String representation of current instance. * * @return String */ Stack.prototype.toString = function toString() { "use strict"; return f("[Object Stack], having %d popup(s)" % this.length); }; webshot/inst/casperjs/modules/casper.js0000644000176200001440000027320613030514600020001 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global __utils__, CasperError, console, exports, phantom, patchRequire, require:true*/ var require = patchRequire(require); var colorizer = require('colorizer'); var events = require('events'); var fs = require('fs'); var http = require('http'); var mouse = require('mouse'); var pagestack = require('pagestack'); var qs = require('querystring'); var tester = require('tester'); var utils = require('utils'); var f = utils.format; var defaultUserAgent = phantom.defaultPageSettings.userAgent .replace(/(PhantomJS|SlimerJS)/, f("CasperJS/%s", phantom.casperVersion) + '+$&'); exports.create = function create(options) { "use strict"; // This is a bit of a hack to check if one is trying to override the preconfigured // casper instance from within a test environment. if (phantom.casperTest && window.casper) { console.error("Fatal: you can't override the preconfigured casper instance in a test environment."); console.error("Docs: http://docs.casperjs.org/en/latest/testing.html#test-command-args-and-options"); phantom.exit(1); } return new Casper(options); }; /** * Shortcut to build an XPath selector object. * * @param String expression The XPath expression * @return Object * @see http://casperjs.org/selectors.html */ function selectXPath(expression) { "use strict"; return { type: 'xpath', path: expression, toString: function() { return this.type + ' selector: ' + this.path; } }; } exports.selectXPath = selectXPath; /** * Main Casper object. * * @param Object options Casper options */ var Casper = function Casper(options) { "use strict"; /*eslint max-statements:0*/ // init & checks if (!(this instanceof Casper)) { return new Casper(options); } // default options this.defaults = { clientScripts: [], colorizerType: 'Colorizer', exitOnError: true, logLevel: "error", httpStatusHandlers: {}, safeLogs: true, onAlert: null, onDie: null, onError: null, onLoadError: null, onPageInitialized: null, onResourceReceived: null, onResourceRequested: null, onRunComplete: function _onRunComplete() { this.exit(); }, onStepComplete: null, onStepTimeout: function _onStepTimeout(timeout, stepNum) { this.die("Maximum step execution timeout exceeded for step " + stepNum); }, onTimeout: function _onTimeout(timeout) { this.die(f("Script timeout of %dms reached, exiting.", timeout)); }, onWaitTimeout: function _onWaitTimeout(timeout) { this.die(f("Wait timeout of %dms expired, exiting.", timeout)); }, page: null, pageSettings: { localToRemoteUrlAccessEnabled: true, userAgent: defaultUserAgent }, remoteScripts: [], silentErrors: false, stepTimeout: null, timeout: null, verbose: false, retryTimeout: 20, waitTimeout: 5000, clipRect : null, viewportSize : null }; // options this.options = utils.mergeObjects(this.defaults, options); // factories this.cli = phantom.casperArgs; this.options.logLevel = this.cli.get('log-level', this.options.logLevel); if (!this.options.verbose) { this.options.verbose = this.cli.has('direct') || this.cli.has('verbose'); } this.colorizer = this.getColorizer(); this.mouse = mouse.create(this); this.popups = pagestack.create(); // properties this.checker = null; this.currentResponse = {}; this.currentUrl = 'about:blank'; this.currentHTTPStatus = null; this.history = []; this.loadInProgress = false; this.navigationRequested = false; this.browserInitializing = false; this.logFormats = {}; this.logLevels = ["debug", "info", "warning", "error"]; this.logStyles = { debug: 'INFO', info: 'PARAMETER', warning: 'COMMENT', error: 'ERROR' }; this.page = null; this.pendingWait = false; this.requestUrl = 'about:blank'; this.resources = []; this.result = { log: [], status: "success", time: 0 }; this.started = false; this.step = -1; this.steps = []; this.waiters = []; this._test = undefined; this.__defineGetter__('test', function() { if (!phantom.casperTest) { throw new CasperError('casper.test property is only available using the `casperjs test` command'); } if (!utils.isObject(this._test)) { this._test = tester.create(this, { concise: this.cli.get('concise') }); } return this._test; }); // init phantomjs error handler this.initErrorHandler(); this.on('error', function(msg, backtrace) { var c = this.getColorizer(); var match = /^(.*): __mod_error(.*):: (.*)/.exec(msg); var notices = []; if (match && match.length === 4) { notices.push(' in module ' + match[2]); msg = match[3]; } /* FIXME: this leads to a recursive on('error'...) trigger, at least in phantomjs2 console.error(c.colorize(msg, 'RED_BAR', 80)); notices.forEach(function(notice) { console.error(c.colorize(notice, 'COMMENT')); }); (backtrace || []).forEach(function(item) { var message = fs.absolute(item.file) + ":" + c.colorize(item.line, "COMMENT"); if (item['function']) { message += " in " + c.colorize(item['function'], "PARAMETER"); } console.error(" " + message); }); */ }); // deprecated feature event handler this.on('deprecated', function onDeprecated(message) { this.warn('[deprecated] ' + message); }); // dispatching an event when instance has been constructed this.emit('init'); // deprecated direct option if (this.cli.has('direct')) { this.emit("deprecated", "--direct option has been deprecated since 1.1; you should use --verbose instead."); } }; // Casper class is an EventEmitter utils.inherits(Casper, events.EventEmitter); /** * Go a step back in browser's history * * @return Casper */ Casper.prototype.back = function back() { "use strict"; this.checkStarted(); return this.then(function() { this.emit('back'); this.page.goBack(); }); }; /** * Encodes a resource using the base64 algorithm synchronously using * client-side XMLHttpRequest. * * NOTE: we cannot use window.btoa() for some strange reasons here. * * @param String url The url to download * @param String method The method to use, optional: default GET * @param String data The data to send, optional * @return string Base64 encoded result */ Casper.prototype.base64encode = function base64encode(url, method, data) { "use strict"; return this.callUtils("getBase64", url, method, data); }; /** * Bypasses `nb` steps. * * @param Integer nb Number of steps to bypass */ Casper.prototype.bypass = function bypass(nb) { "use strict"; var step = this.step, steps = this.steps, last = steps.length, targetStep = Math.min(step + nb, last); this.checkStarted(); this.step = targetStep; this.emit('step.bypassed', targetStep, step); return this; }; /** * Invokes a client side utils object method within the remote page, with arguments. * * @param {String} method Method name * @return {...args} Arguments * @return {Mixed} * @throws {CasperError} If invokation failed. */ Casper.prototype.callUtils = function callUtils(method) { "use strict"; var args = [].slice.call(arguments, 1); var result = this.evaluate(function(method, args) { return __utils__.__call(method, args); }, method, args); if (utils.isObject(result) && result.__isCallError) { throw new CasperError(f("callUtils(%s) with args %s thrown an error: %s", method, args, result.message)); } return result; }; /** * Proxy method for WebPage#render. Adds a clipRect parameter for * automatically set page clipRect setting values and sets it back once * done. If the cliprect parameter is omitted, the full page viewport * area will be rendered. * * @param String targetFile A target filename * @param mixed clipRect An optional clipRect object (optional) * @return Casper */ Casper.prototype.capture = function capture(targetFile, clipRect, imgOptions) { "use strict"; /*eslint max-statements:0*/ this.checkStarted(); var previousClipRect; targetFile = fs.absolute(targetFile); if (clipRect) { if (!utils.isClipRect(clipRect)) { throw new CasperError("clipRect must be a valid ClipRect object."); } previousClipRect = this.page.clipRect; this.page.clipRect = clipRect; this.log(f("Capturing page to %s with clipRect %s", targetFile, JSON.stringify(clipRect)), "debug"); } else { this.log(f("Capturing page to %s", targetFile), "debug"); } if (!this.page.render(this.filter('capture.target_filename', targetFile) || targetFile, imgOptions)) { this.log(f("Failed to save screenshot to %s; please check permissions", targetFile), "error"); } else { this.log(f("Capture saved to %s", targetFile), "info"); this.emit('capture.saved', targetFile); } if (previousClipRect) { this.page.clipRect = previousClipRect; } return this; }; /** * Returns a Base64 representation of a binary image capture of the current * page, or an area within the page, in a given format. * * Supported image formats are `bmp`, `jpg`, `jpeg`, `png`, `ppm`, `tiff`, * `xbm` and `xpm`. * * @param String format The image format * @param String|Object|undefined selector DOM CSS3/XPath selector or clipRect object (optional) * @return Casper */ Casper.prototype.captureBase64 = function captureBase64(format, area) { "use strict"; /*eslint max-statements:0*/ this.checkStarted(); var base64, previousClipRect, formats = ['bmp', 'jpg', 'jpeg', 'png', 'ppm', 'tiff', 'xbm', 'xpm']; if (formats.indexOf(format.toLowerCase()) === -1) { throw new CasperError(f('Unsupported format "%s"', format)); } if (utils.isClipRect(area)) { // if area is a clipRect object this.log(f("Capturing base64 %s representation of %s", format, utils.serialize(area)), "debug"); previousClipRect = this.page.clipRect; this.page.clipRect = area; base64 = this.page.renderBase64(format); } else if (utils.isValidSelector(area)) { // if area is a selector string or object this.log(f("Capturing base64 %s representation of %s", format, area), "debug"); base64 = this.captureBase64(format, this.getElementBounds(area)); } else { // whole page capture this.log(f("Capturing base64 %s representation of page", format), "debug"); base64 = this.page.renderBase64(format); } if (previousClipRect) { this.page.clipRect = previousClipRect; } return base64; }; /** * Captures the page area matching the provided selector. * * @param String targetFile Target destination file path. * @param String selector DOM CSS3/XPath selector * @return Casper */ Casper.prototype.captureSelector = function captureSelector(targetFile, selector, imgOptions) { "use strict"; return this.capture(targetFile, this.getElementBounds(selector), imgOptions); }; /** * Checks for any further navigation step to process. * * @param Casper self A self reference * @param function onComplete An options callback to apply on completion */ Casper.prototype.checkStep = function checkStep(self, onComplete) { "use strict"; if (self.pendingWait || self.loadInProgress || self.navigationRequested || self.browserInitializing) { return; } var step = self.steps[self.step++]; if (utils.isFunction(step)) { return self.runStep(step); } self.result.time = new Date().getTime() - self.startTime; self.log(f("Done %s steps in %dms", self.steps.length, self.result.time), "info"); clearInterval(self.checker); self.step -= 1; self.emit('run.complete'); try { if (utils.isFunction(onComplete)) { onComplete.call(self, self); } else if (utils.isFunction(self.options.onRunComplete)) { self.options.onRunComplete.call(self, self); } } catch (error) { self.emit('complete.error', error); if (!self.options.silentErrors) { throw error; } } }; /** * Checks if this instance is started. * * @return Boolean * @throws CasperError */ Casper.prototype.checkStarted = function checkStarted() { "use strict"; if (!this.started) { throw new CasperError(f("Casper is not started, can't execute `%s()`", checkStarted.caller.name)); } }; /** * Clears the current page execution environment context. Useful to avoid * having previously loaded DOM contents being still active (refs #34). * * Think of it as a way to stop javascript execution within the remote DOM * environment. * * @return Casper */ Casper.prototype.clear = function clear() { "use strict"; this.checkStarted(); this.page.content = ''; return this; }; /** * Emulates a click on the element from the provided selector using the mouse * pointer, if possible. * * In case of success, `true` is returned, `false` otherwise. * * @param String selector A DOM CSS3 compatible selector * @param String target A HTML target '_blank','_self','_parent','_top','framename' (optional) * @param Number x X position (optional) * @param Number y Y position (optional) * @return Boolean */ Casper.prototype.click = function click(selector, x, y) { "use strict"; this.checkStarted(); var success = this.mouseEvent('mousedown', selector, x, y) && this.mouseEvent('mouseup', selector, x, y); success = success && this.mouseEvent('click', selector, x, y); this.evaluate(function(selector) { var element = __utils__.findOne(selector); if (element) { element.focus(); } }, selector); this.emit('click', selector); return success; }; /** * Emulates a click on the element having `label` as innerText. The first * element matching this label will be selected, so use with caution. * * @param String label Element innerText value * @param String tag An element tag name (eg. `a` or `button`) (optional) * @return Boolean */ Casper.prototype.clickLabel = function clickLabel(label, tag) { "use strict"; this.checkStarted(); tag = tag || "*"; var escapedLabel = utils.quoteXPathAttributeString(label); var selector = selectXPath(f('//%s[text()=%s]', tag, escapedLabel)); return this.click(selector); }; /** * Configures HTTP authentication parameters. Will try parsing auth credentials from * the passed location first, then check for configured settings if any. * * @param String location Requested url * @param Object settings Request settings * @return Casper */ Casper.prototype.configureHttpAuth = function configureHttpAuth(location, settings) { "use strict"; var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i); this.checkStarted(); if (httpAuthMatch) { this.page.settings.userName = httpAuthMatch[1]; this.page.settings.password = httpAuthMatch[2]; } else if (utils.isObject(settings) && settings.username) { this.page.settings.userName = settings.username; this.page.settings.password = settings.password; } else { return; } this.emit('http.auth', this.page.settings.userName, this.page.settings.password); this.log("Setting HTTP authentication for user " + this.page.settings.userName, "info"); return this; }; /** * Creates a step definition. * * @param Function fn The step function to call * @param Object options Step options * @return Function The final step function */ Casper.prototype.createStep = function createStep(fn, options) { "use strict"; if (!utils.isFunction(fn)) { throw new CasperError("createStep(): a step definition must be a function"); } fn.options = utils.isObject(options) ? options : {}; this.emit('step.created', fn); return fn; }; /** * Logs the HTML code of the current page. * * @param String selector A DOM CSS3/XPath selector (optional) * @param Boolean outer Whether to fetch outer HTML contents (default: false) * @return Casper */ Casper.prototype.debugHTML = function debugHTML(selector, outer) { "use strict"; this.checkStarted(); return this.echo(this.getHTML(selector, outer)); }; /** * Logs the textual contents of the current page. * * @return Casper */ Casper.prototype.debugPage = function debugPage() { "use strict"; this.checkStarted(); this.echo(this.evaluate(function _evaluate() { return document.body.textContent || document.body.innerText; })); return this; }; /** * Exit phantom on failure, with a logged error message. * * @param String message An optional error message * @param Number status An optional exit status code (must be > 0) * @return Casper */ Casper.prototype.die = function die(message, status) { "use strict"; this.result.status = "error"; this.result.time = new Date().getTime() - this.startTime; if (!utils.isString(message) || !message.length) { message = "Suite explicitly interrupted without any message given."; } this.log(message, "error"); this.echo(message, "ERROR"); this.emit('die', message, status); if (utils.isFunction(this.options.onDie)) { this.options.onDie.call(this, this, message, status); } return this.exit(~~status > 0 ? ~~status : 1); }; /** * Downloads a resource and saves it on the filesystem. * * @param String url The url of the resource to download * @param String targetPath The destination file path * @param String method The HTTP method to use (default: GET) * @param String data Optional data to pass performing the request * @return Casper */ Casper.prototype.download = function download(url, targetPath, method, data) { "use strict"; this.checkStarted(); var cu = require('clientutils').create(utils.mergeObjects({}, this.options)); try { fs.write(targetPath, cu.decode(this.base64encode(url, method, data)), 'wb'); this.emit('downloaded.file', targetPath); this.log(f("Downloaded and saved resource in %s", targetPath)); } catch (e) { this.emit('downloaded.error', url); this.log(f("Error while downloading %s to %s: %s", url, targetPath, e), "error"); } return this; }; /** * Iterates over the values of a provided array and execute a callback for each * item. * * @param Array array * @param Function fn Callback: function(casper, item, index) * @return Casper */ Casper.prototype.each = function each(array, fn) { "use strict"; if (!utils.isArray(array)) { this.log("each() only works with arrays", "error"); return this; } array.forEach(function _forEach(item, i) { fn.call(this, this, item, i); }, this); return this; }; /** * Iterates over the values of a provided array and adds a step for each item. * * @param Array array * @param Function then Step: function(response); item will be attached to response.data * @return Casper */ Casper.prototype.eachThen = function each(array, then) { "use strict"; if (!utils.isArray(array)) { this.log("eachThen() only works with arrays", "error"); return this; } array.forEach(function _forEach(item) { this.then(function() { this.then(this.createStep(then, {data: item})); }); }, this); return this; }; /** * Prints something to stdout. * * @param String text A string to echo to stdout * @param String style An optional style name * @param Number pad An optional pad value * @return Casper */ Casper.prototype.echo = function echo(text, style, pad) { "use strict"; if (!utils.isString(text)) { try { text = text.toString(); } catch (e) { try { text = utils.serialize(text); } catch (e2) { text = ''; } } } var message = style ? this.colorizer.colorize(text, style, pad) : text; console.log(this.filter('echo.message', message) || message); return this; }; /** * Evaluates an expression in the page context, a bit like what * WebPage#evaluate does, but the passed function can also accept * parameters if a context Object is also passed: * * casper.evaluate(function(username, password) { * document.querySelector('#username').value = username; * document.querySelector('#password').value = password; * document.querySelector('#submit').click(); * }, 'Bazoonga', 'baz00nga'); * * @param Function fn The function to be evaluated within current page DOM * @param Object context Object containing the parameters to inject into the function * @return mixed * @see WebPage#evaluate */ Casper.prototype.evaluate = function evaluate(fn, context) { "use strict"; this.checkStarted(); // check whether javascript is enabled !! if (this.options.pageSettings.javascriptEnabled === false) { throw new CasperError("evaluate() requires javascript to be enabled"); } // preliminary checks if (!utils.isFunction(fn) && !utils.isString(fn)) { // phantomjs allows functions defs as string throw new CasperError("evaluate() only accepts functions or strings"); } // ensure client utils are always injected this.injectClientUtils(); // function context if (arguments.length === 1) { return utils.clone(this.page.evaluate(fn)); } else if (arguments.length === 2) { // check for closure signature if it matches context if (utils.isObject(context) && eval(fn).length === Object.keys(context).length) { context = utils.objectValues(context); } else { context = [context]; } } else { // phantomjs-style signature context = [].slice.call(arguments, 1); } return utils.clone(this.page.evaluate.apply(this.page, [fn].concat(context))); }; /** * Evaluates an expression within the current page DOM and die() if it * returns false. * * @param function fn The expression to evaluate * @param String message The error message to log * @param Number status An optional exit status code (must be > 0) * * @return Casper */ Casper.prototype.evaluateOrDie = function evaluateOrDie(fn, message, status) { "use strict"; this.checkStarted(); if (!this.evaluate(fn)) { return this.die(message, status); } return this; }; /** * Checks if an element matching the provided DOM CSS3/XPath selector exists in * current page DOM. * * @param String selector A DOM CSS3/XPath selector * @return Boolean */ Casper.prototype.exists = function exists(selector) { "use strict"; this.checkStarted(); return this.callUtils("exists", selector); }; /** * Exits phantom. * * @param Number status Status * @return Casper */ Casper.prototype.exit = function exit(status) { "use strict"; this.emit('exit', status); setTimeout(function() { phantom.exit(status); }, 0); }; /** * Fetches plain text contents contained in the DOM element(s) matching a given CSS3/XPath * selector. * * @param String selector A DOM CSS3/XPath selector * @return String */ Casper.prototype.fetchText = function fetchText(selector) { "use strict"; this.checkStarted(); return this.callUtils("fetchText", selector); }; /** * Fills a form with provided field values. * * @param String selector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Object options The fill settings (optional) */ Casper.prototype.fillForm = function fillForm(selector, vals, options) { "use strict"; this.checkStarted(); var selectorType = options && options.selectorType || "names", submit = !!(options && options.submit); this.emit('fill', selector, vals, options); var fillResults = this.evaluate(function _evaluate(selector, vals, selectorType) { try { return __utils__.fill(selector, vals, selectorType); } catch (exception) { return {exception: exception.toString()}; } }, selector, vals, selectorType); if (!fillResults) { throw new CasperError("Unable to fill form"); } else if (fillResults && fillResults.exception) { throw new CasperError("Unable to fill form: " + fillResults.exception); } else if (fillResults.errors.length > 0) { throw new CasperError(f('Errors encountered while filling form: %s', fillResults.errors.join('; '))); } // File uploads if (fillResults.files && fillResults.files.length > 0) { if (utils.isObject(selector) && selector.type === 'xpath') { this.warn('Filling file upload fields is currently not supported using ' + 'XPath selectors; Please use a CSS selector instead.'); } else { fillResults.files.forEach(function _forEach(file) { if (!file || !file.path) { return; } var paths = (utils.isArray(file.path) && file.path.length > 0) ? file.path : [file.path]; paths.map(function(filePath) { if (!fs.exists(filePath)) { throw new CasperError('Cannot upload nonexistent file: ' + filePath); } },this); var fileFieldSelector; if (file.type === "names") { fileFieldSelector = [selector, 'input[name="' + file.selector + '"]'].join(' '); } else if (file.type === "css" || file.type === "labels") { fileFieldSelector = [selector, file.selector].join(' '); } this.page.uploadFile(fileFieldSelector, paths); }.bind(this)); } } // Form submission? if (submit) { this.evaluate(function _evaluate(selector) { var form = __utils__.findOne(selector); var method = (form.getAttribute('method') || "GET").toUpperCase(); var action = form.getAttribute('action') || "unknown"; __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info'); var event = document.createEvent('Event'); event.initEvent('submit', true, true); if (!form.dispatchEvent(event)) { __utils__.log('unable to submit form', 'warning'); return; } if (typeof form.submit === "function") { form.submit(); } else { // http://www.spiration.co.uk/post/1232/Submit-is-not-a-function form.submit.click(); } }, selector); } return this; }; /** * Fills a form with provided field values using the Name attribute. * * @param String formSelector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Boolean submit Submit the form? */ Casper.prototype.fillNames = function fillNames(formSelector, vals, submit) { "use strict"; return this.fillForm(formSelector, vals, { submit: submit, selectorType: 'names' }); }; /** * Fills a form with provided field values using associated label text. * * @param String formSelector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Boolean submit Submit the form? */ Casper.prototype.fillLabels = function fillLabels(formSelector, vals, submit) { "use strict"; return this.fillForm(formSelector, vals, { submit: submit, selectorType: 'labels' }); }; /** * Fills a form with provided field values using CSS3 selectors. * * @param String formSelector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Boolean submit Submit the form? */ Casper.prototype.fillSelectors = function fillSelectors(formSelector, vals, submit) { "use strict"; return this.fillForm(formSelector, vals, { submit: submit, selectorType: 'css' }); }; /** * Fills a form with provided field values using the Name attribute by default. * * @param String formSelector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Boolean submit Submit the form? */ Casper.prototype.fill = Casper.prototype.fillNames; /** * Fills a form with provided field values using XPath selectors. * * @param String formSelector A DOM CSS3/XPath selector to the target form to fill * @param Object vals Field values * @param Boolean submit Submit the form? */ Casper.prototype.fillXPath = function fillXPath(formSelector, vals, submit) { "use strict"; return this.fillForm(formSelector, vals, { submit: submit, selectorType: 'xpath' }); }; /** * Go a step forward in browser's history * * @return Casper */ Casper.prototype.forward = function forward() { "use strict"; this.checkStarted(); return this.then(function() { this.emit('forward'); this.page.goForward(); }); }; /** * Creates a new Colorizer instance. Sets `Casper.options.type` to change the * colorizer type name (see the `colorizer` module). * * @return Object */ Casper.prototype.getColorizer = function getColorizer() { "use strict"; return colorizer.create(this.options.colorizerType || 'Colorizer'); }; /** * Retrieves current page contents, dealing with exotic other content types than HTML. * * @return String */ Casper.prototype.getPageContent = function getPageContent() { "use strict"; this.checkStarted(); var contentType = utils.getPropertyPath(this, 'currentResponse.contentType'); if (!utils.isString(contentType) || contentType.indexOf("text/html") !== -1) { return this.page.frameContent; } // FIXME: with slimerjs this will work only for // text/* and application/json content types. // see FIXME in slimerjs src/modules/webpageUtils.jsm getWindowContent return this.page.framePlainText; }; /** * Retrieves current page contents in plain text. * * @return String */ Casper.prototype.getPlainText = function getPlainText() { "use strict"; this.checkStarted(); return this.page.framePlainText; }; /** * Retrieves current document url. * * @return String */ Casper.prototype.getCurrentUrl = function getCurrentUrl() { "use strict"; this.checkStarted(); try { if (this.options.pageSettings.javascriptEnabled === false) { return this.page.url; } else { return utils.decodeUrl(this.evaluate(function _evaluate() { return document.location.href; })); } } catch (e) { // most likely the current page object has been "deleted" (think closed popup) if (/deleted QObject/.test(e.message)) return ""; throw e; } }; /** * Retrieves the value of an attribute on the first element matching the provided * DOM CSS3/XPath selector. * * @param String selector A DOM CSS3/XPath selector * @param String attribute The attribute name to lookup * @return String The requested DOM element attribute value */ Casper.prototype.getElementAttribute = Casper.prototype.getElementAttr = function getElementAttr(selector, attribute) { "use strict"; this.checkStarted(); return this.evaluate(function _evaluate(selector, attribute) { return __utils__.findOne(selector).getAttribute(attribute); }, selector, attribute); }; /** * Retrieves the value of an attribute for each element matching the provided * DOM CSS3/XPath selector. * * @param String selector A DOM CSS3/XPath selector * @param String attribute The attribute name to lookup * @return Array */ Casper.prototype.getElementsAttribute = Casper.prototype.getElementsAttr = function getElementsAttr(selector, attribute) { "use strict"; this.checkStarted(); return this.evaluate(function _evaluate(selector, attribute) { return [].map.call(__utils__.findAll(selector), function(element) { return element.getAttribute(attribute); }); }, selector, attribute); }; /** * Retrieves boundaries for a DOM element matching the provided DOM CSS3/XPath selector. * * @param String selector A DOM CSS3/XPath selector * @return Object */ Casper.prototype.getElementBounds = function getElementBounds(selector) { "use strict"; this.checkStarted(); if (!this.exists(selector)) { throw new CasperError("No element matching selector found: " + selector); } var zoomFactor = this.page.zoomFactor || 1; var clipRect = this.callUtils("getElementBounds", selector); if (zoomFactor !== 1) { for (var prop in clipRect) { if (clipRect.hasOwnProperty(prop)) { clipRect[prop] = clipRect[prop] * zoomFactor; } } } if (!utils.isClipRect(clipRect)) { throw new CasperError('Could not fetch boundaries for element matching selector: ' + selector); } return clipRect; }; /** * Retrieves information about the node matching the provided selector. * * @param String|Objects selector CSS3/XPath selector * @return Object */ Casper.prototype.getElementInfo = function getElementInfo(selector) { "use strict"; this.checkStarted(); if (!this.exists(selector)) { throw new CasperError(f("Cannot get informations from %s: element not found.", selector)); } return this.callUtils("getElementInfo", selector); }; /** * Retrieves information about the nodes matching the provided selector. * * @param String|Objects selector CSS3/XPath selector * @return Array */ Casper.prototype.getElementsInfo = function getElementsInfo(selector) { "use strict"; this.checkStarted(); if (!this.exists(selector)) { throw new CasperError(f("Cannot get information from %s: no elements found.", selector)); } return this.callUtils("getElementsInfo", selector); }; /** * Retrieves boundaries for all the DOM elements matching the provided DOM CSS3/XPath selector. * * @param String selector A DOM CSS3/XPath selector * @return Array */ Casper.prototype.getElementsBounds = function getElementsBounds(selector) { "use strict"; this.checkStarted(); if (!this.exists(selector)) { throw new CasperError("No element matching selector found: " + selector); } var zoomFactor = this.page.zoomFactor || 1; var clipRects = this.callUtils("getElementsBounds", selector); if (zoomFactor !== 1) { Array.prototype.forEach.call(clipRects, function(clipRect) { for (var prop in clipRect) { if (clipRect.hasOwnProperty(prop)) { clipRect[prop] = clipRect[prop] * zoomFactor; } } }); } return clipRects; }; /** * Retrieves a given form all of its field values. * * @param String selector A DOM CSS3/XPath selector * @return Object */ Casper.prototype.getFormValues = function(selector) { "use strict"; this.checkStarted(); if (!this.exists(selector)) { throw new CasperError(f('Form matching selector "%s" not found', selector)); } return this.callUtils("getFormValues", selector); }; /** * Retrieves global variable. * * @param String name The name of the global variable to retrieve * @return mixed */ Casper.prototype.getGlobal = function getGlobal(name) { "use strict"; this.checkStarted(); var result = this.evaluate(function _evaluate(name) { var result = {}; try { result.value = JSON.stringify(window[name]); } catch (e) { var message = "Unable to JSON encode window." + name + ": " + e; __utils__.log(message, "error"); result.error = message; } return result; }, name); if (!utils.isObject(result)) { throw new CasperError(f('Could not retrieve global value for "%s"', name)); } else if ('error' in result) { throw new CasperError(result.error); } else if (utils.isString(result.value)) { return JSON.parse(result.value); } }; /** * Retrieves current HTML code matching the provided CSS3/XPath selector. * Returns the HTML contents for the whole page if no arg is passed. * * @param String selector A DOM CSS3/XPath selector * @param Boolean outer Whether to fetch outer HTML contents (default: false) * @return String */ Casper.prototype.getHTML = function getHTML(selector, outer) { "use strict"; this.checkStarted(); if (!selector) { return this.page.frameContent; } if (!this.exists(selector)) { throw new CasperError("No element matching selector found: " + selector); } return this.evaluate(function getSelectorHTML(selector, outer) { var element = __utils__.findOne(selector); return outer ? element.outerHTML : element.innerHTML; }, selector, !!outer); }; /** * Retrieves current page title, if any. * * @return String */ Casper.prototype.getTitle = function getTitle() { "use strict"; this.checkStarted(); return this.evaluate(function _evaluate() { return document.title; }); }; /** * Handles received HTTP resource. * * @param Object resource PhantomJS HTTP resource */ Casper.prototype.handleReceivedResource = function(resource) { "use strict"; /*eslint max-statements:0*/ if (resource.stage !== "end") { return; } this.resources.push(resource); var checkUrl = ((phantom.casperEngine === 'phantomjs' && utils.ltVersion(phantom.version, '2.1.0')) || (phantom.casperEngine === 'slimerjs' && utils.ltVersion(phantom.version, '0.10.0'))) ? utils.decodeUrl(resource.url) : resource.url; if (checkUrl !== this.requestUrl) { return; } this.currentHTTPStatus = null; this.currentResponse = {}; if (utils.isHTTPResource(resource)) { this.emit('page.resource.received', resource); this.currentResponse = resource; this.currentHTTPStatus = resource.status; this.emit('http.status.' + resource.status, resource); if (utils.isObject(this.options.httpStatusHandlers) && resource.status in this.options.httpStatusHandlers && utils.isFunction(this.options.httpStatusHandlers[resource.status])) { this.options.httpStatusHandlers[resource.status].call(this, this, resource); } } this.currentUrl = resource.url; this.emit('location.changed', resource.url); }; /** * Initializes PhantomJS error handler. * */ Casper.prototype.initErrorHandler = function initErrorHandler() { "use strict"; var casper = this; phantom.onError = function phantom_onError(msg, backtrace) { casper.emit('error', msg, backtrace); if (casper.options.exitOnError === true) { casper.exit(1); } }; }; /** * Injects configured local client scripts. * * @return Casper */ Casper.prototype.injectClientScripts = function injectClientScripts() { "use strict"; this.checkStarted(); if (!this.options.clientScripts) { return; } if (utils.isString(this.options.clientScripts)) { this.options.clientScripts = [this.options.clientScripts]; } if (!utils.isArray(this.options.clientScripts)) { throw new CasperError("The clientScripts option must be an array"); } this.options.clientScripts.forEach(function _forEach(script) { if (this.page.injectJs(script)) { this.log(f('Automatically injected %s client side', script), "debug"); } else { this.warn(f('Failed injecting %s client side', script)); } }.bind(this)); return this; }; /** * Injects Client-side utilities in current page context. * */ Casper.prototype.injectClientUtils = function injectClientUtils() { "use strict"; this.checkStarted(); var clientUtilsInjected = this.page.evaluate(function() { return typeof __utils__ === "object"; }); if (true === clientUtilsInjected) { return; } var clientUtilsPath = require('fs').pathJoin(phantom.casperPath, 'modules', 'clientutils.js'); if (true === this.page.injectJs(clientUtilsPath)) { this.log("Successfully injected Casper client-side utilities", "debug"); } else { this.warn("Failed to inject Casper client-side utilities"); } // ClientUtils and Casper shares the same options // These are not the lines I'm the most proud of in my life, but it works. /*global __options*/ this.page.evaluate(function() { window.__utils__ = new window.ClientUtils(__options); }.toString().replace('__options', JSON.stringify(this.options))); }; /** * Loads and include remote client scripts to current page. * * @return Casper */ Casper.prototype.includeRemoteScripts = function includeRemoteScripts() { "use strict"; var numScripts = this.options.remoteScripts.length, loaded = 0; if (numScripts === 0) { return this; } this.waitStart(); this.options.remoteScripts.forEach(function(scriptUrl) { this.log(f("Loading remote script: %s", scriptUrl), "debug"); this.page.includeJs(scriptUrl, function() { loaded++; this.log(f("Remote script %s loaded", scriptUrl), "debug"); if (loaded === numScripts) { this.log("All remote scripts loaded.", "debug"); this.waitDone(); } }.bind(this)); }.bind(this)); return this; }; /** * Logs a message. * * @param String message The message to log * @param String level The log message level (from Casper.logLevels property) * @param String space Space from where the logged event occurred (default: "phantom") * @return Casper */ Casper.prototype.log = function log(message, level, space) { "use strict"; level = level && this.logLevels.indexOf(level) > -1 ? level : "debug"; space = space ? space : "phantom"; if (level === "error" && utils.isFunction(this.options.onError)) { this.options.onError.call(this, this, message, space); } if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) { return this; // skip logging } var entry = { level: level, space: space, message: message, date: new Date().toString() }; if (level in this.logFormats && utils.isFunction(this.logFormats[level])) { message = this.logFormats[level](message, level, space); } else { message = f('%s [%s] %s', this.colorizer.colorize(f('[%s]', level), this.logStyles[level]), space, message); } if (this.options.verbose) { this.echo(this.filter('log.message', message) || message); // direct output } this.result.log.push(entry); this.emit('log', entry); return this; }; /** * Emulates an event on the element from the provided selector using the mouse * pointer, if possible. * * In case of success, `true` is returned, `false` otherwise. * * @param String type Type of event to emulate * @param String selector A DOM CSS3 compatible selector * @param {Number} x X position * @param {Number} y Y position * @return Boolean */ Casper.prototype.mouseEvent = function mouseEvent(type, selector, x, y) { "use strict"; this.checkStarted(); this.log("Mouse event '" + type + "' on selector: " + selector, "debug"); if (!this.exists(selector)) { throw new CasperError(f("Cannot dispatch %s event on nonexistent selector: %s", type, selector)); } if (this.callUtils("mouseEvent", type, selector, x, y)) { return true; } // fallback onto native QtWebKit mouse events try { return this.mouse.processEvent(type, selector, x, y); } catch (e) { this.log(f("Couldn't emulate '%s' event on %s: %s", type, selector, e), "error"); } return false; }; /** * Performs an HTTP request, with optional settings. * * Available settings are: * * - String method: The HTTP method to use * - Object data: The data to use to perform the request, eg. {foo: 'bar'} * - Object headers: Custom request headers object, eg. {'Cache-Control': 'max-age=0'} * * @param String location The url to open * @param Object settings The request settings (optional) * @return Casper */ Casper.prototype.open = function open(location, settings) { "use strict"; /*eslint max-statements:0*/ var baseCustomHeaders = this.page.customHeaders, customHeaders = settings && settings.headers || {}; this.checkStarted(); settings = utils.isObject(settings) ? settings : {}; settings.method = settings.method || "get"; // http method // taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302 var methods = ["get", "head", "put", "post", "delete"]; if (settings.method && (!utils.isString(settings.method) || methods.indexOf(settings.method.toLowerCase()) === -1)) { throw new CasperError("open(): settings.method must be part of " + methods.join(', ')); } // http data if (settings.data) { if (utils.isObject(settings.data)) { // query object if (settings.headers && settings.headers["Content-Type"] && settings.headers["Content-Type"].match(/application\/json/)) { settings.data = JSON.stringify(settings.data); // convert object to JSON notation } else { settings.data = qs.encode(settings.data); // escapes all characters except alphabetic, decimal digits and ,-_.!~*'() } } else if (!utils.isString(settings.data)) { throw new CasperError("open(): invalid request settings data value: " + settings.data); } } // clean location location = utils.cleanUrl(location); // current request url this.configureHttpAuth(location, settings); this.requestUrl = this.filter('open.location', location) || location; this.emit('open', this.requestUrl, settings); this.log(f('opening url: %s, HTTP %s', this.requestUrl, settings.method.toUpperCase()), "debug"); // reset resources this.resources = []; // custom headers this.page.customHeaders = utils.mergeObjects(utils.clone(baseCustomHeaders), customHeaders); // perfom request this.browserInitializing = true; var phantomJsSettings = { operation: settings.method, data: settings.data }; // override any default encoding setting in phantomjs if ('encoding' in settings) { phantomJsSettings.encoding = settings.encoding; } this.page.openUrl(this.requestUrl, phantomJsSettings, this.page.settings); // revert base custom headers this.page.customHeaders = baseCustomHeaders; return this; }; /** * Reloads current page. * * @param Function then a next step function * @return Casper */ Casper.prototype.reload = function reload(then) { "use strict"; this.checkStarted(); this.then(function() { this.page.reload(); }); if (utils.isFunction(then)) { this.then(this.createStep(then)); } return this; }; /** * Repeats a step a given number of times. * * @param Number times Number of times to repeat step * @aram function then The step closure * @return Casper * @see Casper#then */ Casper.prototype.repeat = function repeat(times, then) { "use strict"; for (var i = 0; i < times; i++) { this.then(then); } return this; }; /** * Checks if a given resource was loaded by the remote page. * * @param Function/String/RegExp test A test function, string or regular expression. * In case a string is passed, url matching will be tested. * @return Boolean */ Casper.prototype.resourceExists = function resourceExists(test) { "use strict"; this.checkStarted(); var testFn; switch (utils.betterTypeOf(test)) { case "string": testFn = function _testResourceExists_String(res) { return res.url.indexOf(test) !== -1 && res.status !== 404; }; break; case "regexp": testFn = function _testResourceExists_Regexp(res) { return test.test(res.url) && res.status !== 404; }; break; case "function": testFn = test; break; default: throw new CasperError("Invalid type"); } return this.resources.some(testFn); }; /** * Runs the whole suite of steps. * * @param function onComplete an optional callback * @param Number time an optional amount of milliseconds for interval checking * @return Casper */ Casper.prototype.run = function run(onComplete, time) { "use strict"; this.checkStarted(); if (!this.steps || this.steps.length < 1) { throw new CasperError('No steps defined, aborting'); } this.log(f("Running suite: %d step%s", this.steps.length, this.steps.length > 1 ? "s" : ""), "info"); this.emit('run.start'); this.checker = setInterval(this.checkStep, (time ? time: this.options.retryTimeout), this, onComplete); return this; }; /** * Runs a step. * * @param Function step */ Casper.prototype.runStep = function runStep(step) { "use strict"; /*eslint max-statements:0*/ this.checkStarted(); var skipLog = utils.isObject(step.options) && step.options.skipLog === true, stepInfo = f("Step %s %d/%d", step.name || "anonymous", this.step, this.steps.length), stepResult; function getCurrentSuiteId(casper) { try { return casper.test.getCurrentSuiteId(); } catch (e) { return casper.step; } } if (!skipLog && /^http/.test(this.getCurrentUrl())) { this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info"); } if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) { var stepTimeoutCheckInterval = setInterval(function _check(self, start, stepNum) { if (new Date().getTime() - start > self.options.stepTimeout) { if (getCurrentSuiteId(self) === stepNum) { self.emit('step.timeout', stepNum, self.options.onStepTimeout); if (utils.isFunction(self.options.onStepTimeout)) { self.options.onStepTimeout.call(self, self.options.stepTimeout, stepNum); } } clearInterval(stepTimeoutCheckInterval); } }, this.options.stepTimeout, this, new Date().getTime(), getCurrentSuiteId(this)); } this.emit('step.start', step); if (this.currentResponse) { if (step.options && (typeof step.options.data !== 'undefined')) { this.currentResponse.data = step.options.data; } else { this.currentResponse.data = null; } } try { stepResult = step.call(this, this.currentResponse); if (utils.isFunction(this.options.onStepComplete)) { this.options.onStepComplete.call(this, this, stepResult); } } catch (err) { this.emit('step.error', err); if (!this.options.silentErrors) { throw err; } } if (!skipLog) { this.emit('step.complete', stepResult); this.log(stepInfo + f(": done in %dms.", new Date().getTime() - this.startTime), "info"); } }; /** * Sends keys to given element. * * Options: * * - eventType: "keypress", "keyup" or "keydown" (default: "keypress") * - modifiers: a string defining the key modifiers, eg. "alt", "alt+shift" * * @param String selector A DOM CSS3 compatible selector * @param String keys A string representing the sequence of char codes to send * @param Object options Options * @return Casper */ Casper.prototype.sendKeys = function(selector, keys, options) { "use strict"; this.checkStarted(); options = utils.mergeObjects({ eventType: 'keypress', reset: false }, options || {}); var elemInfos = this.getElementInfo(selector), tag = elemInfos.nodeName.toLowerCase(), type = utils.getPropertyPath(elemInfos, 'attributes.type'), supported = ["color", "date", "datetime", "datetime-local", "email", "hidden", "month", "number", "password", "range", "search", "tel", "text", "time", "url", "week"], isTextInput = false, isTextArea = tag === 'textarea', isValidInput = tag === 'input' && (typeof type === 'undefined' || supported.indexOf(type) !== -1), isContentEditable = !!elemInfos.attributes.contenteditable; if (isTextArea || isValidInput || isContentEditable) { // clicking on the input element brings it focus isTextInput = true; this.click(selector); } if (options.reset) { this.evaluate(function(selector) { __utils__.setField(__utils__.findOne(selector), ''); }, selector); this.click(selector); } var modifiers = utils.computeModifier(options && options.modifiers, this.page.event.modifier); this.page.sendEvent(options.eventType, keys, null, null, modifiers); if (isTextInput && !options.keepFocus) { // remove the focus this.evaluate(function(selector) { __utils__.findOne(selector).blur(); }, selector); } return this; }; /** * Scrolls current document to x, y coordinates. * * @param {Number} x X position * @param {Number} y Y position * @return {Casper} */ Casper.prototype.scrollTo = function(x, y) { "use strict"; this.callUtils("scrollTo", x, y); return this; }; /** * Scrolls current document up to its bottom. * * @return {Casper} */ Casper.prototype.scrollToBottom = function scrollToBottom() { "use strict"; this.callUtils("scrollToBottom"); return this; }; /** * Sets current page content. * * @param String content Desired page content * @return Casper */ Casper.prototype.setContent = function setContent(content) { "use strict"; this.checkStarted(); this.page.content = content; return this; }; /** * Sets a value to form field by CSS3, XPath selector or by its name attribute or label text. * * @param String|Object selector CSS3, XPath, name or label * @param Mixed value Value being set * @param String|Object form (optional) CSS3 or XPath selector of form * @param Object options Options to setFieldValue, it accepts: * - options.selectorType name|labes|xpath|css3 - type of selector, where * CSS3 and XPath(object) is autodetected (need not be set) */ Casper.prototype.setFieldValue = function setFieldValue(selector, value, form, options) { "use strict"; this.checkStarted(); var selectorType = options && options.selectorType; var result = this.evaluate(function _evaluate(selector, value, form, selectorType) { if (selectorType) { selector = __utils__.makeSelector(selector, selectorType); } return __utils__.setFieldValue(selector, value, form); }, selector, value, form, selectorType); if (!result) { throw new CasperError("Unable to set field '" + selector + " to value: " + value) + ' in setFieldValue().'; } }; /** * Alias to setFieldValue() with implicit type name * * @param String name Name of form field * @param Mixed value Value being set * @param String|Object form (optional) CSS3 or XPath selector of form */ Casper.prototype.setFieldValueName = function setFieldValueName(name, value, form) { "use strict"; this.checkStarted(); this.setFieldValue(name, value, form, {'selectorType': 'name'}); }; /** * Alias to setFieldValue() with implicit type label * * @param String name Name of form field * @param Mixed value Value being set * @param String|Object form (optional) CSS3 or XPath selector of form */ Casper.prototype.setFieldValueLabel = function setFieldValueLabel(label, value, form) { "use strict"; this.checkStarted(); this.setFieldValue(label, value, form, {'selectorType': 'label'}); }; /** * Sets current WebPage instance the credentials for HTTP authentication. * * @param String username * @param String password * @return Casper */ Casper.prototype.setHttpAuth = function setHttpAuth(username, password) { "use strict"; this.checkStarted(); this.page.settings.userName = username; this.page.settings.password = password; return this; }; /** * Configures and starts Casper. * * @param String location An optional location to open on start * @param function then Next step function to execute on page loaded (optional) * @return Casper */ Casper.prototype.start = function start(location, then) { "use strict"; /*eslint max-statements:0*/ this.emit('starting'); this.log('Starting...', "info"); this.startTime = new Date().getTime(); this.currentResponse = {}; this.history = []; this.popups = pagestack.create(); this.steps = []; this.step = 0; // Option checks if (this.logLevels.indexOf(this.options.logLevel) < 0) { this.log(f("Unknown log level '%d', defaulting to 'warning'", this.options.logLevel), "warning"); this.options.logLevel = "warning"; } if (!utils.isWebPage(this.page)) { this.page = this.mainPage = utils.isWebPage(this.options.page) ? this.options.page : createPage(this); } this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings); if (utils.isClipRect(this.options.clipRect)) { this.page.clipRect = this.options.clipRect; } if (utils.isObject(this.options.viewportSize)) { this.page.viewportSize = this.options.viewportSize; } // timeout handling if (utils.isNumber(this.options.timeout) && this.options.timeout > 0) { this.log(f("Execution timeout set to %dms", this.options.timeout), "info"); setTimeout(function _check(self) { self.emit('timeout'); if (utils.isFunction(self.options.onTimeout)) { self.options.onTimeout.call(self, self.options.timeout); } }, this.options.timeout, this); } this.started = true; this.emit('started'); if (utils.isString(location) && location.length > 0) { return this.thenOpen(location, utils.isFunction(then) ? then : this.createStep(function _step() { this.log("start page is loaded", "debug"); }, {skipLog: true})); } return this; }; /** * Returns the current status of current instance * * @param Boolean asString Export status object as string * @return Object */ Casper.prototype.status = function status(asString) { "use strict"; var properties = ['currentHTTPStatus', 'loadInProgress', 'navigationRequested', 'options', 'pendingWait', 'requestUrl', 'started', 'step', 'url']; var currentStatus = {}; properties.forEach(function(property) { currentStatus[property] = this[property]; }.bind(this)); return asString === true ? utils.dump(currentStatus) : currentStatus; }; /** * Schedules the next step in the navigation process. * * @param function step A function to be called as a step * @return Casper */ Casper.prototype.then = function then(step) { "use strict"; this.checkStarted(); if (!utils.isFunction(step)) { throw new CasperError("You can only define a step as a function"); } // check if casper is running if (this.checker === null) { // append step to the end of the queue step.level = 0; this.steps.push(step); } else { // insert substep a level deeper try { step.level = this.steps[this.step - 1].level + 1; } catch (e) { step.level = 0; } var insertIndex = this.step; while (this.steps[insertIndex] && step.level === this.steps[insertIndex].level) { insertIndex++; } this.steps.splice(insertIndex, 0, step); } this.emit('step.added', step); return this; }; /** * Adds a new navigation step for clicking on a provided link selector * and execute an optional next step. * * @param String selector A DOM CSS3 compatible selector * @param Function then Next step function to execute on page loaded (optional) * @return Casper * @see Casper#click * @see Casper#then */ Casper.prototype.thenClick = function thenClick(selector, then) { "use strict"; this.checkStarted(); this.then(function _step() { this.click(selector); }); return utils.isFunction(then) ? this.then(then) : this; }; /** * Adds a new navigation step to perform code evaluation within the * current retrieved page DOM. * * @param function fn The function to be evaluated within current page DOM * @param Array args... The rest of arguments passed to fn * @return Casper * @see Casper#evaluate */ Casper.prototype.thenEvaluate = function thenEvaluate(fn) { "use strict"; this.checkStarted(); var args = arguments; return this.then(function _step() { this.evaluate.apply(this, args); }); }; /** * Adds a new navigation step for opening the provided location. * * @param String location The URL to load * @param function then Next step function to execute on page loaded (optional) * @return Casper * @see Casper#open */ Casper.prototype.thenOpen = function thenOpen(location, settings, then) { "use strict"; this.checkStarted(); if (!(settings && !utils.isFunction(settings))) { then = settings; settings = null; } this.then(this.createStep(function _step() { this.open(location, settings); }, { skipLog: true })); return utils.isFunction(then) ? this.then(then) : this; }; /** * Adds a step which bypasses `nb` steps. * * @param Integer nb Number of steps to bypass */ Casper.prototype.thenBypass = function thenBypass(nb) { "use strict"; return this.then(function _thenBypass() { this.bypass(nb); }); }; /** * Bypass `nb` steps if condition is true. * * @param Mixed condition Test condition * @param Integer nb Number of steps to bypass */ Casper.prototype.thenBypassIf = function thenBypassIf(condition, nb) { "use strict"; return this.then(function _thenBypassIf() { if (utils.isFunction(condition)) { condition = condition.call(this); } if (utils.isTruthy(condition)) { this.bypass(nb); } }); }; /** * Bypass `nb` steps if condition is false. * * @param Mixed condition Test condition * @param Integer nb Number of tests to bypass */ Casper.prototype.thenBypassUnless = function thenBypassUnless(condition, nb) { "use strict"; return this.then(function _thenBypassUnless() { if (utils.isFunction(condition)) { condition = condition.call(this); } if (utils.isFalsy(condition)) { this.bypass(nb); } }); }; /** * Adds a new navigation step for opening and evaluate an expression * against the DOM retrieved from the provided location. * * @param String location The url to open * @param function fn The function to be evaluated within current page DOM * @param Array args... The rest of arguments will passed to the evaluate function * @return Casper * @see Casper#evaluate * @see Casper#open */ Casper.prototype.thenOpenAndEvaluate = function thenOpenAndEvaluate(location, fn) { "use strict"; this.checkStarted(); var args = [].slice.call(arguments, 1); return this.thenOpen(location).thenEvaluate.apply(this, args); }; /** * Returns a string representation of current instance * * @return String */ Casper.prototype.toString = function toString() { "use strict"; return '[object Casper], currently at ' + this.getCurrentUrl(); }; /** * Clear all wait processes. * * @return Casper */ Casper.prototype.unwait = function unwait() { "use strict"; this.waiters.forEach(function(interval) { if (interval) { clearInterval(interval); } }); this.waiters = []; return this; }; /** * Sets the user-agent string currently used when requesting urls. * * @param String userAgent User agent string * @return String */ Casper.prototype.userAgent = function userAgent(agent) { "use strict"; this.options.pageSettings.userAgent = agent; if (this.started && this.page) { this.page.settings.userAgent = agent; } return this; }; /** * Changes the current viewport size. That operation is asynchronous as the page * has to reflow its contents accordingly. * * @param Number width The viewport width, in pixels * @param Number height The viewport height, in pixels * @param Function then Next step to process (optional) * @return Casper */ Casper.prototype.viewport = function viewport(width, height, then) { "use strict"; this.checkStarted(); if (!utils.isNumber(width) || !utils.isNumber(height) || width <= 0 || height <= 0) { throw new CasperError(f("Invalid viewport: %dx%d", width, height)); } this.page.viewportSize = { width: width, height: height }; // setting the viewport could cause a redraw and it can take // time. At least for Gecko, we should wait a bit, even // if this time could not be enough. var time = (phantom.casperEngine === 'slimerjs'?400:100); return this.then(function _step() { this.waitStart(); setTimeout(function _check(self) { self.waitDone(); self.emit('viewport.changed', [width, height]); if (utils.isFunction(then)){ self.then(then); } }, time, this); }); }; /** * Checks if an element matching the provided DOM CSS3/XPath selector is visible * current page DOM by checking that offsetWidth and offsetHeight are * both non-zero. * * @param String selector A DOM CSS3/XPath selector * @return Boolean */ Casper.prototype.visible = function visible(selector) { "use strict"; this.checkStarted(); return this.callUtils("visible", selector); }; /** * Checks if all elements matching the provided DOM CSS3/XPath selector are visible * * @param String selector A DOM CSS3/XPath selector * @return Boolean */ Casper.prototype.allVisible = function allVisible(selector) { "use strict"; this.checkStarted(); return this.callUtils("allVisible", selector); }; /** * Displays a warning message onto the console and logs the event. Also emits a * `warn` event with the message passed. * * @param String message * @return Casper */ Casper.prototype.warn = function warn(message) { "use strict"; this.log(message, "warning", "phantom"); var formatted = f.apply(null, ["⚠  " + message].concat([].slice.call(arguments, 1))); this.emit('warn', message); return this.echo(formatted, 'COMMENT'); }; /** * Helper functions needed in wait*() methods. Casts timeout argument to integer and checks if next step * function is really a function and if it has been given (if required - depending on isThenRequired flag). * * @param Number timeout The max amount of time to wait, in milliseconds * @param Function then Next step to process (optional or required, depending on isThenRequired flag) * @param String methodName Name of the method, inside of which the helper has been called * @param Number defaultTimeout The default max amount of time to wait, in milliseconds (optional) * @param Boolean isThenRequired Determines if the next step function should be considered as required * @returns Number */ function getTimeoutAndCheckNextStepFunction(timeout, then, methodName, defaultTimeout, isThenRequired) { if (isThenRequired || then) { var isFunction = utils.isFunction(then); // Optimization to perform "isFunction" check only once. if (isThenRequired && !isFunction) { throw new CasperError(methodName + "() needs a step function"); } else if (then && !isFunction) { throw new CasperError(methodName + "() next step definition must be a function"); } } timeout = ~~timeout || ~~defaultTimeout; if (timeout < 0) { throw new CasperError(methodName + "() only accepts an integer >= 0 as a timeout value"); } return timeout; } /** * Adds a new step that will wait for a given amount of time (expressed * in milliseconds) before processing an optional next one. * * @param Number timeout The max amount of time to wait, in milliseconds * @param Function then Next step to process (optional) * @return Casper */ Casper.prototype.wait = function wait(timeout, then) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'wait'); return this.then(function _step() { this.waitStart(); setTimeout(function _check(self) { self.log(f("wait() finished waiting for %dms.", timeout), "info"); if (then) { try { then.call(self, self); } catch (error) { self.emit('wait.error', error); if (!self.options.silentErrors) { throw error; } } } self.waitDone(); }, timeout, this); }); }; Casper.prototype.waitStart = function waitStart() { "use strict"; this.emit('wait.start'); this.pendingWait = true; }; Casper.prototype.waitDone = function waitDone() { "use strict"; this.emit('wait.done'); this.pendingWait = false; }; /** * Waits until a function returns true to process a next step. * * @param Function testFx A function to be evaluated for returning condition satisfecit * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @param Object details A property bag of information about the condition being waited on (optional) * @return Casper */ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout, details) { "use strict"; this.checkStarted(); if (!utils.isFunction(testFx)) { throw new CasperError("waitFor() needs a test function"); } timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitFor', this.options.waitTimeout); details = details || { testFx: testFx }; return this.then(function _step() { this.waitStart(); var start = new Date().getTime(); var condition = false; var interval = setInterval(function _check(self) { /*eslint max-statements: [1, 20]*/ if ((new Date().getTime() - start < timeout) && !condition) { condition = testFx.call(self, self); return; } self.waitDone(); if (!condition) { self.log("Casper.waitFor() timeout", "warning"); var onWaitTimeout = onTimeout ? onTimeout : self.options.onWaitTimeout; self.emit('waitFor.timeout', timeout, details); clearInterval(interval); // refs #383 if (!utils.isFunction(onWaitTimeout)) { throw new CasperError('Invalid timeout function'); } try { return onWaitTimeout.call(self, timeout, details); } catch (error) { self.emit('waitFor.timeout.error', error); if (!self.options.silentErrors) { throw error; } } } else { self.log(f("waitFor() finished in %dms.", new Date().getTime() - start), "info"); clearInterval(interval); if (then) { self.then(then); } } }, this.options.retryTimeout, this); this.waiters.push(interval); }); }; /** * Waits until any alert is triggered. * * @param Function then The next step to perform (required) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitForAlert = function(then, onTimeout, timeout) { "use strict"; timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForAlert', undefined, true); var message; function alertCallback(msg) { message = msg; } this.once("remote.alert", alertCallback); return this.waitFor(function isAlertReceived() { return message !== undefined; }, function onAlertReceived() { this.then(this.createStep(then, {data: message})); }, onTimeout, timeout); }; /** * Waits for a popup page having its url matching the provided pattern to be opened * and loaded. * * @param String|RegExp urlPattern The popup url pattern * @param Function then The next step function (optional) * @param Function onTimeout Function to call on operation timeout (optional) * @param Number timeout Timeout in milliseconds (optional) * @return Casper */ Casper.prototype.waitForPopup = function waitForPopup(urlPattern, then, onTimeout, timeout) { "use strict"; timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForPopup'); return this.waitFor(function() { try { this.popups.find(urlPattern); return true; } catch (e) { return false; } }, then, onTimeout, timeout, { popup: urlPattern }); }; /** * Waits until a given resource is loaded * * @param String/Function/RegExp test A function to test if the resource exists. * A string will be matched against the resources url. * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitForResource = function waitForResource(test, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForResource', this.options.waitTimeout); return this.waitFor(function _check() { return this.resourceExists(test); }, then, onTimeout, timeout, { resource: test }); }; /** * Waits for a given url to be loaded. * * @param String|RegExp url The url to wait for * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @return Casper */ Casper.prototype.waitForUrl = function waitForUrl(url, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForUrl', this.options.waitTimeout); return this.waitFor(function _check() { if (utils.isString(url)) { return this.getCurrentUrl().indexOf(url) !== -1; } else if (utils.isRegExp(url)) { return url.test(this.getCurrentUrl()); } throw new CasperError('invalid url argument'); }, then, onTimeout, timeout, { url: url }); }; /** * Waits until an element matching the provided DOM CSS3/XPath selector exists in * remote DOM to process a next step. * * @param String selector A DOM CSS3/XPath selector * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitForSelector = function waitForSelector(selector, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForSelector', this.options.waitTimeout); return this.waitFor(function _check() { return this.exists(selector); }, then, onTimeout, timeout, { selector: selector }); }; /** * Waits until the page contains given HTML text or matches a given RegExp. * * @param String|RegExp pattern Text or RegExp to wait for * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitForText = function(pattern, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForText', this.options.waitTimeout); return this.waitFor(function _check() { var content = this.getPageContent(); if (utils.isRegExp(pattern)) { return pattern.test(content); } return content.indexOf(pattern) !== -1; }, then, onTimeout, timeout, { text: pattern }); }; /** * Waits until the text on an element matching the provided DOM CSS3/XPath selector * is changed to a different value. * * @param String selector A DOM CSS3/XPath selector * @param Function then The next step to preform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitForSelectorTextChange = function(selector, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitForSelectorTextChange', this.options.waitTimeout); var currentSelectorText = this.fetchText(selector); return this.waitFor(function _check() { return currentSelectorText !== this.fetchText(selector); }, then, onTimeout, timeout, { selectorTextChange: selector }); }; /** * Waits until an element matching the provided DOM CSS3/XPath selector does not * exist in the remote DOM to process a next step. * * @param String selector A DOM CSS3/XPath selector * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitWhileSelector = function waitWhileSelector(selector, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitWhileSelector', this.options.waitTimeout); return this.waitFor(function _check() { return !this.exists(selector); }, then, onTimeout, timeout, { selector: selector, waitWhile: true }); }; /** * Waits until an element matching the provided DOM CSS3/XPath selector is * visible in the remote DOM to process a next step. * * @param String selector A DOM CSS3/XPath selector * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitUntilVisible = function waitUntilVisible(selector, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitUntilVisible', this.options.waitTimeout); return this.waitFor(function _check() { return this.visible(selector); }, then, onTimeout, timeout, { visible: selector }); }; /** * Waits until an element matching the provided DOM CSS3/XPath selector is no * longer visible in remote DOM to process a next step. * * @param String selector A DOM CSS3/XPath selector * @param Function then The next step to perform (optional) * @param Function onTimeout A callback function to call on timeout (optional) * @param Number timeout The max amount of time to wait, in milliseconds (optional) * @return Casper */ Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, onTimeout, timeout) { "use strict"; this.checkStarted(); timeout = getTimeoutAndCheckNextStepFunction(timeout, then, 'waitWhileVisible', this.options.waitTimeout); return this.waitFor(function _check() { return !this.visible(selector); }, then, onTimeout, timeout, { visible: selector, waitWhile: true }); }; /** * Makes the provided frame page as the currently active one. Note that the * active page will be reverted when finished. * * @param String|Number frameInfo Target frame name or number * @param Function then Next step function * @return Casper */ Casper.prototype.withFrame = function withFrame(frameInfo, then) { "use strict"; this.then(function _step() { if (utils.isNumber(frameInfo)) { if (frameInfo > this.page.childFramesCount() - 1) { throw new CasperError(f('Frame number "%d" is out of bounds.', frameInfo)); } } else if (this.page.childFramesName().indexOf(frameInfo) === -1) { throw new CasperError(f('No frame named "%s" was found.', frameInfo)); } // make the frame page the currently active one this.page.switchToChildFrame(frameInfo); }); try { this.then(then); } catch (e) { // revert to main page on error this.warn("Error while processing frame step: " + e); this.page.switchToParentFrame(); throw e; } return this.then(function _step() { // revert to main page this.page.switchToParentFrame(); }); }; /** * Makes the provided frame page as the currently active one. Note that the * active page will be reverted when finished. * * @param String|RegExp|WebPage popup Target frame page information * @param Function then Next step function * @return Casper */ Casper.prototype.withPopup = function withPopup(popupInfo, then) { "use strict"; this.then(function _step() { var popupPage = this.popups.find(popupInfo); if (!utils.isFunction(then)) { throw new CasperError("withPopup() requires a step function."); } // make the popup page the currently active one this.page = popupPage; }); try { this.then(then); } catch (e) { // revert to main page on error this.log("error while processing popup step: " + e, "error"); this.page = this.mainPage; throw e; } return this.then(function _step() { // revert to main page this.page = this.mainPage; }); }; /** * Allow user to create a new page object after calling a casper.page.close() * @return WebPage */ Casper.prototype.newPage = function newPage() { "use strict"; this.checkStarted(); this.page.close(); this.page = this.mainPage = createPage(this); this.page.settings = utils.mergeObjects(this.page.settings, this.options.pageSettings); if (utils.isClipRect(this.options.clipRect)) { this.page.clipRect = this.options.clipRect; } if (utils.isObject(this.options.viewportSize)) { this.page.viewportSize = this.options.viewportSize; } return this.page; }; /** * Changes the current page zoom factor. * * @param Number factor The zoom factor * @return Casper */ Casper.prototype.zoom = function zoom(factor) { "use strict"; this.checkStarted(); if (!utils.isNumber(factor) || factor <= 0) { throw new CasperError("Invalid zoom factor: " + factor); } this.page.zoomFactor = factor; return this; }; /** * Extends Casper's prototype with provided one. * * @param Object proto Prototype methods to add to Casper * @deprecated * @since 0.6 */ Casper.extend = function(proto) { "use strict"; this.emit("deprecated", "Casper.extend() has been deprecated since 0.6; check the docs"); if (!utils.isObject(proto)) { throw new CasperError("extends() only accept objects as prototypes"); } utils.mergeObjects(Casper.prototype, proto); }; exports.Casper = Casper; /** * Creates a new WebPage instance for Casper use. * * @param Casper casper A Casper instance * @return WebPage */ function createPage(casper) { /*eslint max-statements:0*/ "use strict"; var page = require('webpage').create(); page.onAlert = function onAlert(message) { casper.log('[alert] ' + message, "info", "remote"); casper.emit('remote.alert', message); if (utils.isFunction(casper.options.onAlert)) { casper.options.onAlert.call(casper, casper, message); } }; page.onConfirm = function onConfirm(message) { if ('page.confirm' in casper._filters) { return casper.filter('page.confirm', message); } return true; }; page.onConsoleMessage = function onConsoleMessage(msg) { // client utils casper console message var consoleTest = /^\[casper\.echo\]\s?([\s\S]*)/.exec(msg); if (consoleTest && consoleTest.length === 2) { casper.echo(consoleTest[1]); return; // don't trigger remote.message event for these } // client utils log messages var logLevel = "info", logTest = /^\[casper:(\w+)\]\s?([\s\S]*)/m.exec(msg); if (logTest && logTest.length === 3) { logLevel = logTest[1]; msg = logTest[2]; casper.log(msg, logLevel, "remote"); } else { casper.emit('remote.message', msg); } }; page.onCallback = function onCallback(data){ casper.emit('remote.callback',data); }; page.onError = function onError(msg, trace) { casper.emit('page.error', msg, trace); }; page.onInitialized = function onInitialized() { casper.emit('page.initialized', page); if (utils.isFunction(casper.options.onPageInitialized)) { casper.log("Post-configuring WebPage instance", "debug"); casper.options.onPageInitialized.call(casper, page); } }; page.onLoadStarted = function onLoadStarted() { // in some case, there is no navigation requested event, so // be sure that browserInitializing is false to not block checkStep() casper.browserInitializing = false; casper.loadInProgress = true; casper.emit('load.started'); }; page.onLoadFinished = function onLoadFinished(status) { /*eslint max-statements:0*/ if (status !== "success") { casper.emit('load.failed', { status: status, http_status: casper.currentHTTPStatus, url: casper.requestUrl }); var message = 'Loading resource failed with status=' + status; if (casper.currentHTTPStatus) { message += f(' (HTTP %d)', casper.currentHTTPStatus); } message += ': ' + casper.requestUrl; casper.log(message, "warning"); casper.navigationRequested = false; casper.browserInitializing = false; if (utils.isFunction(casper.options.onLoadError)) { casper.options.onLoadError.call(casper, casper, casper.requestUrl, status); } } // local client scripts casper.injectClientScripts(); // remote client scripts casper.includeRemoteScripts(); // Client-side utils injection casper.injectClientUtils(); // history casper.history.push(casper.getCurrentUrl()); casper.emit('load.finished', status); casper.loadInProgress = false; }; page.onNavigationRequested = function onNavigationRequested(url, type, willNavigate, isMainFrame) { casper.log(f('Navigation requested: url=%s, type=%s, willNavigate=%s, isMainFrame=%s', url, type, willNavigate, isMainFrame), "debug"); casper.browserInitializing = false; if (isMainFrame && casper.requestUrl !== url) { var currentUrl = casper.requestUrl; var newUrl = url; var pos = currentUrl.indexOf('#'); if (pos !== -1) { currentUrl = currentUrl.substring(0, pos); } pos = newUrl.indexOf('#'); if (pos !== -1) { newUrl = newUrl.substring(0, pos); } // for URLs that are only different by their hash part // or if navigation locked (willNavigate == false) // don't turn navigationRequested to true, because // there will not be loadStarted, loadFinished events // so it could cause issues (for exemple, checkStep that // do no execute the next step -> infinite loop on checkStep) if (willNavigate && currentUrl !== newUrl) casper.navigationRequested = true; if (willNavigate) { casper.requestUrl = url; } } casper.emit('navigation.requested', url, type, willNavigate, isMainFrame); }; page.onPageCreated = function onPageCreated(popupPage) { casper.emit('popup.created', popupPage); popupPage.onLoadFinished = function onLoadFinished() { // SlimerJS needs this line of code because of issue // https://github.com/laurentj/slimerjs/issues/48 // else checkStep turns into an infinite loop // after clicking on an casper.navigationRequested = false; casper.popups.push(popupPage); casper.emit('popup.loaded', popupPage); }; popupPage.onClosing = function onClosing(closedPopup) { casper.popups.clean(); casper.emit('popup.closed', closedPopup); }; }; page.onPrompt = function onPrompt(message, value) { return casper.filter('page.prompt', message, value); }; page.onResourceReceived = function onResourceReceived(resource) { http.augmentResponse(resource); casper.emit('resource.received', resource); if (utils.isFunction(casper.options.onResourceReceived)) { casper.options.onResourceReceived.call(casper, casper, resource); } casper.handleReceivedResource(resource); }; page.onResourceRequested = function onResourceRequested(requestData, request) { casper.emit('resource.requested', requestData, request); var checkUrl = ((phantom.casperEngine === 'phantomjs' && utils.ltVersion(phantom.version, '2.1.0')) || (phantom.casperEngine === 'slimerjs' && utils.ltVersion(phantom.version, '0.10.0'))) ? utils.decodeUrl(requestData.url) : requestData.url; if (checkUrl === casper.requestUrl) { casper.emit('page.resource.requested', requestData, request); } if (utils.isFunction(casper.options.onResourceRequested)) { casper.options.onResourceRequested.call(casper, casper, requestData, request); } }; page.onResourceError = function onResourceError(resourceError) { casper.emit('resource.error', resourceError); }; page.onUrlChanged = function onUrlChanged(url) { casper.log(f('url changed to "%s"', url), "debug"); casper.navigationRequested = false; casper.emit('url.changed', url); }; casper.emit('page.created', page); return page; } webshot/inst/casperjs/modules/cli.js0000644000176200001440000001224613030514600017266 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global CasperError, console, exports, phantom, patchRequire, require:true*/ var require = patchRequire(require); var utils = require('utils'); var system = require('system'); /** * Extracts, normalize and organize PhantomJS CLI arguments in a dedicated * Object. * * @param array phantomArgs system.args value * @return Object */ exports.parse = function parse(phantomArgs) { "use strict"; var extract = { args: [], options: {}, raw: { args: [], options: {} }, drop: function drop(what) { if (utils.isNumber(what)) { // deleting an arg by its position this.args = this.args.filter(function _filter(arg, index) { return index !== what; }); // raw if ('raw' in this) { this.raw.args = this.raw.args.filter(function _filter(arg, index) { return index !== what; }); } } else if (utils.isString(what)) { // deleting an arg by its value this.args = this.args.filter(function _filter(arg) { return arg !== what; }); // deleting an option by its name (key) delete this.options[what]; // raw if ('raw' in this) { this.raw.args = this.raw.args.filter(function _filter(arg) { return arg !== what; }); delete this.raw.options[what]; } } else { throw new CasperError("Cannot drop argument of type " + typeof what); } }, has: function has(what) { if (utils.isNumber(what)) { return what in this.args; } if (utils.isString(what)) { return what in this.options; } throw new CasperError("Unsupported cli arg tester " + typeof what); }, get: function get(what, def) { if (utils.isNumber(what)) { return what in this.args ? this.args[what] : def; } if (utils.isString(what)) { return what in this.options ? this.options[what] : def; } throw new CasperError("Unsupported cli arg getter " + typeof what); } }; phantomArgs.forEach(function _forEach(arg) { if (arg.indexOf('--') === 0) { // named option var optionMatch = arg.match(/^--(.*?)=(.*)/i); if (optionMatch) { extract.options[optionMatch[1]] = castArgument(optionMatch[2]); extract.raw.options[optionMatch[1]] = optionMatch[2]; } else { // flag var flagMatch = arg.match(/^--(.*)/); if (flagMatch) { extract.options[flagMatch[1]] = extract.raw.options[flagMatch[1]] = true; } } } else { // positional arg extract.args.push(castArgument(arg)); extract.raw.args.push(arg); } }); extract.raw = utils.mergeObjects(extract.raw, { drop: function() { return extract.drop.apply(extract, arguments); }, has: extract.has, get: extract.get }); return extract; }; /** * Cast a string argument to its typed equivalent. * * @param String arg * @return Mixed */ function castArgument(arg) { "use strict"; if (arg.match(/^-?\d+$/)) { return parseInt(arg, 10); } else if (arg.match(/^-?\d+\.\d+$/)) { return parseFloat(arg); } else if (arg.match(/^(true|false)$/i)) { return arg.trim().toLowerCase() === "true" ? true : false; } else { return arg; } } webshot/inst/casperjs/modules/clientutils.js0000644000176200001440000012236213030514600021057 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*global escape, NodeList*/ (function(exports) { "use strict"; exports.create = function create(options) { return new this.ClientUtils(options); }; /** * Casper client-side helpers. */ exports.ClientUtils = function ClientUtils(options) { /*eslint max-statements:0, no-multi-spaces:0*/ // private members var BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var BASE64_DECODE_CHARS = [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 ]; var SUPPORTED_SELECTOR_TYPES = ['css', 'xpath']; // public members this.options = options || {}; this.options.scope = this.options.scope || document; /** * Calls a method part of the current prototype, with arguments. * * @param {String} method Method name * @param {Array} args arguments * @return {Mixed} */ this.__call = function __call(method, args) { if (method === "__call") { return; } try { return this[method].apply(this, args); } catch (err) { err.__isCallError = true; return err; } }; /** * Clicks on the DOM element behind the provided selector. * * @param String selector A CSS3 selector to the element to click * @param {Number} x X position * @param {Number} y Y position * @return Boolean */ this.click = function click(selector, x, y) { return this.mouseEvent('click', selector, x, y); }; /** * Decodes a base64 encoded string. Succeeds where window.atob() fails. * * @param String str The base64 encoded contents * @return string */ this.decode = function decode(str) { /*eslint max-statements:0, complexity:0 */ var c1, c2, c3, c4, i = 0, len = str.length, out = ""; while (i < len) { do { c1 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; } while (i < len && c1 === -1); if (c1 === -1) { break; } do { c2 = BASE64_DECODE_CHARS[str.charCodeAt(i++) & 0xff]; } while (i < len && c2 === -1); if (c2 === -1) { break; } out += String.fromCharCode(c1 << 2 | (c2 & 0x30) >> 4); do { c3 = str.charCodeAt(i++) & 0xff; if (c3 === 61) { return out; } c3 = BASE64_DECODE_CHARS[c3]; } while (i < len && c3 === -1); if (c3 === -1) { break; } out += String.fromCharCode((c2 & 0XF) << 4 | (c3 & 0x3C) >> 2); do { c4 = str.charCodeAt(i++) & 0xff; if (c4 === 61) { return out; } c4 = BASE64_DECODE_CHARS[c4]; } while (i < len && c4 === -1); if (c4 === -1) { break; } out += String.fromCharCode((c3 & 0x03) << 6 | c4); } return out; }; /** * Echoes something to casper console. * * @param String message * @return */ this.echo = function echo(message) { console.log("[casper.echo] " + message); }; /** * Checks if a given DOM element is visible in remove page. * * @param Object element DOM element * @return Boolean */ this.elementVisible = function elementVisible(elem) { var style; try { style = window.getComputedStyle(elem, null); } catch (e) { return false; } var hidden = style.visibility === 'hidden' || style.display === 'none'; if (hidden) { return false; } if (style.display === "inline" || style.display === "inline-block") { return true; } return elem.clientHeight > 0 && elem.clientWidth > 0; }; /** * Base64 encodes a string, even binary ones. Succeeds where * window.btoa() fails. * * @param String str The string content to encode * @return string */ this.encode = function encode(str) { /*eslint max-statements:0 */ var out = "", i = 0, len = str.length, c1, c2, c3; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i === len) { out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4); out += "=="; break; } c2 = str.charCodeAt(i++); if (i === len) { out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4 | (c2 & 0xF0) >> 4); out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2); out += "="; break; } c3 = str.charCodeAt(i++); out += BASE64_ENCODE_CHARS.charAt(c1 >> 2); out += BASE64_ENCODE_CHARS.charAt((c1 & 0x3) << 4 | (c2 & 0xF0) >> 4); out += BASE64_ENCODE_CHARS.charAt((c2 & 0xF) << 2 | (c3 & 0xC0) >> 6); out += BASE64_ENCODE_CHARS.charAt(c3 & 0x3F); } return out; }; /** * Checks if a given DOM element exists in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.exists = function exists(selector) { try { return this.findAll(selector).length > 0; } catch (e) { return false; } }; /** * Fetches innerText within the element(s) matching a given CSS3 * selector. * * @param String selector A CSS3 selector * @return String */ this.fetchText = function fetchText(selector) { var text = '', elements = this.findAll(selector); if (elements && elements.length) { Array.prototype.forEach.call(elements, function _forEach(element) { text += element.textContent || element.innerText || element.value || ''; }); } return text; }; /** * Fills a form with provided field values, and optionally submits it. * * @param HTMLElement|String form A form element, or a CSS3 selector to a form element * @param Object vals Field values * @param String findType Element finder type (css, names, xpath, labels) * @return Object An object containing setting result for each field, including file uploads */ this.fill = function fill(form, vals, findType) { findType = findType || "names"; /*eslint complexity:0*/ var out = { errors: [], fields: [], files: [] }; if (!(form instanceof HTMLElement) || typeof form === "string") { this.log("attempting to fetch form element from selector: '" + form + "'", "info"); try { form = this.findOne(form); } catch (e) { if (e.name === "SYNTAX_ERR") { out.errors.push("invalid form selector provided: '" + form + "'"); return out; } } } if (!form) { out.errors.push("form not found"); return out; } for (var fieldSelector in vals) { if (!vals.hasOwnProperty(fieldSelector)) { continue; } try { out.fields[fieldSelector] = this.setFieldValue(this.makeSelector(fieldSelector, findType), vals[fieldSelector], form); } catch (err) { switch (err.name) { case "FieldNotFound": out.errors.push('Unable to find field element in form: ' + err.toString()); break; case "FileUploadError": out.files.push({ type: findType, selector: findType === "labels" ? '#' + err.id : fieldSelector, path: err.path }); break; default: out.errors.push(err.toString()); } } } return out; }; /** * Finds all DOM elements matching by the provided selector. * * @param String | Object selector CSS3 selector (String only) or XPath object * @param HTMLElement|null scope Element to search child elements within * @return Array|undefined */ this.findAll = function findAll(selector, scope) { scope = scope instanceof HTMLElement ? scope : scope && this.findOne(scope) || this.options.scope; try { var pSelector = this.processSelector(selector); if (pSelector.type === 'xpath') { return this.getElementsByXPath(pSelector.path, scope); } else { return Array.prototype.slice.call(scope.querySelectorAll(pSelector.path)); } } catch (e) { this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error"); } }; /** * Finds a DOM element by the provided selector. * * @param String | Object selector CSS3 selector (String only) or XPath object * @param HTMLElement|null scope Element to search child elements within * @return HTMLElement|undefined */ this.findOne = function findOne(selector, scope) { scope = scope instanceof HTMLElement ? scope : scope && this.findOne(scope) || this.options.scope; try { var pSelector = this.processSelector(selector); if (pSelector.type === 'xpath') { return this.getElementByXPath(pSelector.path, scope); } else { return scope.querySelector(pSelector.path); } } catch (e) { this.log('findOne(): invalid selector provided "' + selector + '":' + e, "error"); } }; /** * Force target on
and tag. * * @param String selector CSS3 selector * @param String A HTML target '_blank','_self','_parent','_top','framename' * @return Boolean */ this.forceTarget = function forceTarget(selector, target) { var elem = this.findOne(selector); while (!!elem && elem.tagName !== 'A' && elem.tagName !== 'FORM' && elem.tagName !== 'BODY'){ elem = elem.parentNode; } if (elem === 'A' || elem === 'FORM') { elem.setAttribute('target', target); return true; } return false; }; /** * Downloads a resource behind an url and returns its base64-encoded * contents. * * @param String url The resource url * @param String method The request method, optional (default: GET) * @param Object data The request data, optional * @return String Base64 contents string */ this.getBase64 = function getBase64(url, method, data) { return this.encode(this.getBinary(url, method, data)); }; /** * Retrieves string contents from a binary file behind an url. Silently * fails but log errors. * * @param String url Url. * @param String method HTTP method. * @param Object data Request parameters. * @return String */ this.getBinary = function getBinary(url, method, data) { try { return this.sendAJAX(url, method, data, false, { overrideMimeType: "text/plain; charset=x-user-defined" }); } catch (e) { if (e.name === "NETWORK_ERR" && e.code === 101) { this.log("getBinary(): Unfortunately, casperjs cannot make" + " cross domain ajax requests", "warning"); } this.log("getBinary(): Error while fetching " + url + ": " + e, "error"); return ""; } }; /** * Retrieves total document height. * http://james.padolsey.com/javascript/get-document-height-cross-browser/ * * @return {Number} */ this.getDocumentHeight = function getDocumentHeight() { return Math.max( Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), Math.max(document.body.offsetHeight, document.documentElement.offsetHeight), Math.max(document.body.clientHeight, document.documentElement.clientHeight) ); }; /** * Retrieves total document width. * http://james.padolsey.com/javascript/get-document-width-cross-browser/ * * @return {Number} */ this.getDocumentWidth = function getDocumentWidth() { return Math.max( Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), Math.max(document.body.offsetWidth, document.documentElement.offsetWidth), Math.max(document.body.clientWidth, document.documentElement.clientWidth) ); }; /** * Retrieves bounding rect coordinates of the HTML element matching the * provided CSS3 selector in the following form: * * {top: y, left: x, width: w, height:, h} * * @param String selector * @return Object or null */ this.getElementBounds = function getElementBounds(selector) { try { var clipRect = this.findOne(selector).getBoundingClientRect(); return { top: clipRect.top, left: clipRect.left, width: clipRect.width, height: clipRect.height }; } catch (e) { this.log("Unable to fetch bounds for element " + selector, "warning"); } }; /** * Retrieves the list of bounding rect coordinates for all the HTML elements matching the * provided CSS3 selector, in the following form: * * [{top: y, left: x, width: w, height:, h}, * {top: y, left: x, width: w, height:, h}, * ...] * * @param String selector * @return Array */ this.getElementsBounds = function getElementsBounds(selector) { var elements = this.findAll(selector); try { return Array.prototype.map.call(elements, function(element) { var clipRect = element.getBoundingClientRect(); return { top: clipRect.top, left: clipRect.left, width: clipRect.width, height: clipRect.height }; }); } catch (e) { this.log("Unable to fetch bounds for elements matching " + selector, "warning"); } }; /** * Retrieves information about the node matching the provided selector. * * @param String|Object selector CSS3/XPath selector * @return Object */ this.getElementInfo = function getElementInfo(selector) { var element = this.findOne(selector); var bounds = this.getElementBounds(selector); var attributes = {}; [].forEach.call(element.attributes, function(attr) { attributes[attr.name.toLowerCase()] = attr.value; }); return { nodeName: element.nodeName.toLowerCase(), attributes: attributes, tag: element.outerHTML, html: element.innerHTML, text: element.textContent || element.innerText, x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height, visible: this.visible(selector) }; }; /** * Retrieves information about the nodes matching the provided selector. * * @param String|Object selector CSS3/XPath selector * @return Array */ this.getElementsInfo = function getElementsInfo(selector) { var bounds = this.getElementsBounds(selector); var eleVisible = this.elementVisible; return [].map.call(this.findAll(selector), function(element, index) { var attributes = {}; [].forEach.call(element.attributes, function(attr) { attributes[attr.name.toLowerCase()] = attr.value; }); return { nodeName: element.nodeName.toLowerCase(), attributes: attributes, tag: element.outerHTML, html: element.innerHTML, text: element.textContent || element.innerText, x: bounds[index].left, y: bounds[index].top, width: bounds[index].width, height: bounds[index].height, visible: eleVisible(element) }; }); }; /** * Retrieves a single DOM element matching a given XPath expression. * * @param String expression The XPath expression * @param HTMLElement|null scope Element to search child elements within * @return HTMLElement or null */ this.getElementByXPath = function getElementByXPath(expression, scope) { scope = scope || this.options.scope; var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); if (a.snapshotLength > 0) { return a.snapshotItem(0); } }; /** * Retrieves all DOM elements matching a given XPath expression. * * @param String expression The XPath expression * @param HTMLElement|null scope Element to search child elements within * @return Array */ this.getElementsByXPath = function getElementsByXPath(expression, scope) { scope = scope || this.options.scope; var nodes = []; var a = document.evaluate(expression, scope, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0; i < a.snapshotLength; i++) { nodes.push(a.snapshotItem(i)); } return nodes; }; /** * Retrieves the value of an element * * @param String inputName The for input name attr value * @param Object options Object with formSelector, optional * @return Mixed */ this.getFieldValue = function getFieldValue(selector, scope) { var self = this; var fields = this.findAll(selector, scope); var type; // for Backward Compatibility if (!(fields instanceof NodeList || fields instanceof Array)) { this.log("attempting to fetch field element from selector: '" + selector + "'", "info"); fields = this.findAll('[name="' + selector + '"]'); } if (fields && fields.length > 1) { type = fields[0].hasAttribute('type') ? fields[0].getAttribute('type') : "other"; fields = [].filter.call(fields, function(elm){ if (elm.nodeName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(elm.getAttribute('type')) !== -1) { return elm.checked; } return true; }); } if (fields.length === 0 ) { return type !== "radio" ? [] : undefined; } if (fields.length > 1 ) { return [].map.call(fields, function(elm) { var ret = self.getField(elm); return ret && type === 'checkbox' ? elm.value : ret; }); } return this.getField(fields[0]); }; /** * Retrieves the value of a form field. * * @param HTMLElement An html element * @return Mixed */ this.getField = function getField(field) { var nodeName, type; if (!(field instanceof HTMLElement)) { var error = new Error('getFieldValue: Invalid field ; only HTMLElement is supported'); error.name = 'FieldNotFound'; throw error; } nodeName = field.nodeName.toLowerCase(); type = field.hasAttribute('type') ? field.getAttribute('type').toLowerCase() : 'text'; if (nodeName === "select" && field.multiple) { return [].filter.call(field.options, function(option){ return !!option.selected; }).map(function(option){ return option.value || option.text; }); } if (type === 'radio') { return field.checked ? field.value : null; } if (type === 'checkbox') { return field.checked; } return field.value || ''; }; /** * Retrieves a given form all of its field values. * * @param HTMLElement|String form A form element, or a CSS3 selector to a form element * @return Object */ this.getFormValues = function getFormValues(form) { var self = this; var values = {}, checked = {}; if (!(form instanceof HTMLElement) || typeof form === "string") { this.log("attempting to fetch form element from selector: '" + form + "'", "info"); try { form = this.findOne(form); } catch (e) { this.log("invalid form selector provided: '" + form + "'"); return {}; } } [].forEach.call(form.elements, function(elm) { var name = elm.getAttribute('name'); var value = self.getField(elm); var multi = !!value && elm.hasAttribute('type') && elm.type === 'checkbox' ? elm.value : value; if (!!name && value !== null && !(elm.type === 'checkbox' && value === false)) { if (typeof values[name] === "undefined") { values[name] = value; checked[name] = multi; } else { if (!Array.isArray(values[name])) { values[name] = [checked[name]]; } values[name].push(multi); } } }); return values; }; /** * Logs a message. Will format the message a way CasperJS will be able * to log phantomjs side. * * @param String message The message to log * @param String level The log level */ this.log = function log(message, level) { console.log("[casper:" + (level || "debug") + "] " + message); }; /** * Makes selector by defined type XPath, Name or Label. Function has same result as selectXPath in Casper module for * XPath type - it makes XPath object. * Function also accepts name attribut of the form filed or can select element by its label text. * * @param String selector Selector of defined type * @param String|null type Type of selector, it can have these values: * css - CSS3 selector - selector is returned trasparently * xpath - XPath selector - return XPath object * name|names - select input of specific name, internally covert to CSS3 selector * label|labels - select input of specific label, internally covert to XPath selector. As selector is label's text used. * @return String|Object */ this.makeSelector = function makeSelector(selector, type){ type = type || 'xpath'; // default type var ret; if (typeof selector === "object") { // selector object (CSS3 | XPath) could by passed selector = selector.path; } switch (type) { case 'css': // do nothing ret = selector; break; case 'name': // convert to css case 'names': ret = '[name="' + selector + '"]'; break; case 'label': // covert to xpath object case 'labels': ret = {type: 'xpath', path: '//*[@id=string(//label[text()="' + selector + '"]/@for)]'}; break; case 'xpath': // covert to xpath object ret = {type: 'xpath', path: selector}; break; default: throw new Error("Unsupported selector type: " + type); } return ret; }; /** * Dispatches a mouse event to the DOM element behind the provided selector. * * @param String type Type of event to dispatch * @param String selector A CSS3 selector to the element to click * @param {Number} x X position * @param {Number} y Y position * @return Boolean */ this.mouseEvent = function mouseEvent(type, selector, x, y) { var elem = this.findOne(selector); if (!elem || !this.elementVisible(elem)) { this.log("mouseEvent(): Couldn't find any element matching '" + selector + "' selector", "error"); return false; } var convertNumberToIntAndPercentToFloat = function (a, def){ return !!a && !isNaN(a) && parseInt(a, 10) || !!a && !isNaN(parseFloat(a)) && parseFloat(a) >= 0 && parseFloat(a) <= 100 && parseFloat(a) / 100 || def; }; try { var evt = document.createEvent("MouseEvents"); var px = convertNumberToIntAndPercentToFloat(x, 0.5), py = convertNumberToIntAndPercentToFloat(y, 0.5); try { var bounds = elem.getBoundingClientRect(); px = Math.floor(bounds.width * (px - (px ^ 0)).toFixed(10)) + (px ^ 0) + bounds.left; py = Math.floor(bounds.height * (py - (py ^ 0)).toFixed(10)) + (py ^ 0) + bounds.top; } catch (e) { px = 1; py = 1; } evt.initMouseEvent(type, true, true, window, 1, 1, 1, px, py, false, false, false, false, type !== "contextmenu" ? 0 : 2, elem); // dispatchEvent return value is false if at least one of the event // handlers which handled this event called preventDefault; // so we cannot returns this results as it cannot accurately informs on the status // of the operation // let's assume the event has been sent ok it didn't raise any error elem.dispatchEvent(evt); return true; } catch (e) { this.log("Failed dispatching " + type + "mouse event on " + selector + ": " + e, "error"); return false; } }; /** * Processes a selector input, either as a string or an object. * * If passed an object, if must be of the form: * * selectorObject = { * type: <'css' or 'xpath'>, * path: * } * * @param String|Object selector The selector string or object * * @return an object containing 'type' and 'path' keys */ this.processSelector = function processSelector(selector) { var selectorObject = { toString: function toString() { return this.type + ' selector: ' + this.path; } }; if (typeof selector === "string") { // defaults to CSS selector selectorObject.type = "css"; selectorObject.path = selector; return selectorObject; } else if (typeof selector === "object") { // validation if (!selector.hasOwnProperty('type') || !selector.hasOwnProperty('path')) { throw new Error("Incomplete selector object"); } else if (SUPPORTED_SELECTOR_TYPES.indexOf(selector.type) === -1) { throw new Error("Unsupported selector type: " + selector.type); } if (!selector.hasOwnProperty('toString')) { selector.toString = selectorObject.toString; } return selector; } throw new Error("Unsupported selector type: " + typeof selector); }; /** * Removes all DOM elements matching a given XPath expression. * * @param String expression The XPath expression * @return Array */ this.removeElementsByXPath = function removeElementsByXPath(expression) { var a = document.evaluate(expression, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0; i < a.snapshotLength; i++) { a.snapshotItem(i).parentNode.removeChild(a.snapshotItem(i)); } }; /** * Scrolls current document to x, y coordinates. * * @param {Number} x X position * @param {Number} y Y position */ this.scrollTo = function scrollTo(x, y) { window.scrollTo(parseInt(x || 0, 10), parseInt(y || 0, 10)); }; /** * Scrolls current document up to its bottom. */ this.scrollToBottom = function scrollToBottom() { this.scrollTo(0, this.getDocumentHeight()); }; /** * Performs an AJAX request. * * @param String url Url. * @param String method HTTP method (default: GET). * @param Object data Request parameters. * @param Boolean async Asynchroneous request? (default: false) * @param Object settings Other settings when perform the ajax request * @return String Response text. */ this.sendAJAX = function sendAJAX(url, method, data, async, settings) { var xhr = new XMLHttpRequest(), dataString = "", dataList = []; method = method && method.toUpperCase() || "GET"; var contentType = settings && settings.contentType || "application/x-www-form-urlencoded"; xhr.open(method, url, !!async); this.log("sendAJAX(): Using HTTP method: '" + method + "'", "debug"); if (settings && settings.overrideMimeType) { xhr.overrideMimeType(settings.overrideMimeType); } if (method === "POST") { if (typeof data === "object") { for (var k in data) { if (data.hasOwnProperty(k)) { dataList.push(encodeURIComponent(k) + "=" + encodeURIComponent(data[k].toString())); } } dataString = dataList.join('&'); this.log("sendAJAX(): Using request data: '" + dataString + "'", "debug"); } else if (typeof data === "string") { dataString = data; } xhr.setRequestHeader("Content-Type", contentType); } xhr.send(method === "POST" ? dataString : null); return xhr.responseText; }; /** * Sets a value to form element by CSS3 or XPath selector. * * With makeSelector() helper can by easily used with name or label selector * @exemple setFieldValue(this.makeSelector('email', 'name'), 'value') * * @param String|Object CSS3|XPath selector * @param Mixed Input value * @param HTMLElement|String|null scope Element to search child elements within * @return bool */ this.setFieldValue = function setFieldValue(selector, value, scope) { var self = this; var fields = this.findAll(selector, scope); var values = value; if (!Array.isArray(value)) { values = [value]; } if (fields && fields.length > 1) { fields = [].filter.call(fields, function(elm){ if (elm.nodeName.toLowerCase() === 'input' && ['checkbox', 'radio'].indexOf(elm.getAttribute('type')) !== -1) { return values.indexOf(elm.getAttribute('value')) !== -1; } return true; }); [].forEach.call(fields, function(elm) { self.setField(elm, value); }); } else { this.setField(fields[0], value); } return true; }; /** * Sets a field value. Fails silently, but log * error messages. * * @param HTMLElement field One element defining a field * @param mixed value The field value to set */ this.setField = function setField(field, value) { /*eslint complexity:0*/ var logValue, out, filter; value = logValue = value || ""; if (!(field instanceof HTMLElement)) { var error = new Error('setField: Invalid field ; only HTMLElement is supported'); error.name = 'FieldNotFound'; throw error; } if (this.options && this.options.safeLogs && field.getAttribute('type') === "password") { // obfuscate password value logValue = new Array(('' + value).length + 1).join("*"); } this.log('Set "' + field.getAttribute('name') + '" field value to ' + logValue, "debug"); try { field.focus(); } catch (e) { this.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning"); } filter = String(field.getAttribute('type') ? field.getAttribute('type') : field.nodeName).toLowerCase(); switch (filter) { case "checkbox": case "radio": field.checked = value ? true : false; break; case "file": throw { name: "FileUploadError", message: "File field must be filled using page.uploadFile", path: value, id: field.id || null }; break; case "select": if (field.multiple) { [].forEach.call(field.options, function(option) { option.selected = value.indexOf(option.value) !== -1; }); // If the values can't be found, try search options text if (field.value === "") { [].forEach.call(field.options, function(option) { option.selected = value.indexOf(option.text) !== -1; }); } } else { // PhantomJS 1.x.x can't handle setting value to '' if (value === "") { field.selectedIndex = -1; } else { field.value = value; } // If the value can't be found, try search options text if (field.value !== value) { [].some.call(field.options, function(option) { option.selected = value === option.text; return value === option.text; }); } } break; default: field.value = value; } ['change', 'input'].forEach(function(name) { var event = document.createEvent("HTMLEvents"); event.initEvent(name, true, true); field.dispatchEvent(event); }); // blur the field try { field.blur(); } catch (err) { this.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning"); } return out; }; /** * Checks if any element matching a given selector is visible in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.visible = function visible(selector) { return [].some.call(this.findAll(selector), this.elementVisible); }; /** * Checks if all elements matching a given selector are visible in remote page. * * @param String selector CSS3 selector * @return Boolean */ this.allVisible = function allVisible(selector) { return [].every.call(this.findAll(selector), this.elementVisible); }; }; })(typeof exports === "object" && !(exports instanceof Element) ? exports : window); webshot/inst/casperjs/modules/utils.js0000644000176200001440000005571313030514600017665 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ var require = patchRequire(require); /** * Provides a better typeof operator equivalent, able to retrieve the array * type. * * CAVEAT: this function does not necessarilly map to classical js "type" names, * notably a `null` will map to "null" instead of "object". * * @param mixed input * @return String * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ */ function betterTypeOf(input) { "use strict"; switch (input) { case undefined: return 'undefined'; case null: return 'null'; default: try { var type = Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); if (type === 'object' && phantom.casperEngine !== "phantomjs" && '__type' in input) { type = input.__type; } // gecko returns window instead of domwindow else if (type === 'window') { return 'domwindow'; } return type; } catch (e) { return typeof input; } } } exports.betterTypeOf = betterTypeOf; /** * Provides a better instanceof operator, capable of checking against the full object prototype hierarchy. * * @param mixed input * @param function constructor * @return String * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model */ function betterInstanceOf(input, constructor) { "use strict"; /*eslint eqeqeq:0 */ if (typeof input == 'undefined' || input == null) { return false; } var inputToTest = input; while (inputToTest != null) { if (inputToTest == constructor.prototype) { return true; } if (typeof inputToTest == 'xml') { return constructor.prototype == document.prototype; } if (typeof inputToTest == 'undefined') { return false; } inputToTest = inputToTest.__proto__; } return equals(input.constructor.name, constructor.name); } exports.betterInstanceOf = betterInstanceOf; /** * Cleans a passed URL. * * @param String url An HTTP URL * @return String */ function cleanUrl(url) { "use strict"; if (url.toLowerCase().indexOf('http') !== 0) { return url; } var a = document.createElement('a'); a.href = url; return a.href; } exports.cleanUrl = cleanUrl; /** * Clones an object. * * @param Mixed o * @return Mixed */ function clone(o) { "use strict"; return JSON.parse(JSON.stringify(o)); } exports.clone = clone; /** * Computes a modifier string to its PhantomJS equivalent. A modifier string is * in the form "ctrl+alt+shift". * * @param String modifierString Modifier string, eg. "ctrl+alt+shift" * @param Object modifiers Modifiers definitions * @return Number */ function computeModifier(modifierString, modifiers) { "use strict"; var modifier = 0, checkKey = function(key) { if (key in modifiers) return; throw new CasperError(format('%s is not a supported key modifier', key)); }; if (!modifierString) return modifier; var keys = modifierString.split('+'); keys.forEach(checkKey); return keys.reduce(function(acc, key) { return acc | modifiers[key]; }, modifier); } exports.computeModifier = computeModifier; /** * Decodes a URL. * @param String url * @return String */ function decodeUrl(url) { "use strict"; try { return decodeURIComponent(url); } catch (e) { /*global unescape*/ return unescape(url); } } exports.decodeUrl = decodeUrl; /** * Dumps a JSON representation of passed value to the console. Used for * debugging purpose only. * * @param Mixed value */ function dump(value) { "use strict"; console.log(serialize(value, 4)); } exports.dump = dump; /** * Tests equality between the two passed arguments. * * @param Mixed v1 * @param Mixed v2 * @param Boolean */ function equals(v1, v2) { "use strict"; if (isFunction(v1)) { return v1.toString() === v2.toString(); } // with Gecko, instanceof is not enough to test object if (v1 instanceof Object || isObject(v1)) { if (!(v2 instanceof Object || isObject(v2)) || Object.keys(v1).length !== Object.keys(v2).length) { return false; } for (var k in v1) { if (!equals(v1[k], v2[k])) { return false; } } return true; } return v1 === v2; } exports.equals = equals; /** * Returns the file extension in lower case. * * @param String file File path * @return string */ function fileExt(file) { "use strict"; try { return file.split('.').pop().toLowerCase().trim(); } catch(e) { return ''; } } exports.fileExt = fileExt; /** * Takes a string and append blanks until the pad value is reached. * * @param String text * @param Number pad Pad value (optional; default: 80) * @return String */ function fillBlanks(text, pad) { "use strict"; pad = pad || 80; if (text.length < pad) { text += new Array(pad - text.length + 1).join(' '); } return text; } exports.fillBlanks = fillBlanks; /** * Formats a string with passed parameters. Ported from nodejs `util.format()`. * * @return String */ function format(f) { "use strict"; var i = 1; var args = arguments; var len = args.length; var str = String(f).replace(/%[sdj%]/g, function _replace(x) { if (i >= len) return x; switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); case '%j': return JSON.stringify(args[i++]); case '%%': return '%'; default: return x; } }); for (var x = args[i]; i < len; x = args[++i]) { if (x === null || typeof x !== 'object') { str += ' ' + x; } else { str += '[obj]'; } } return str; } exports.format = format; /** * Formats a test value. * * @param Mixed value * @return String */ function formatTestValue(value, name) { "use strict"; var formatted = ''; if (value instanceof Error) { formatted += value.message + '\n'; if (value.stack) { formatted += indent(value.stack, 12, '#'); } } else if (name === 'stack') { if (isArray(value)) { formatted += value.map(function(entry) { return format('in %s() in %s:%d', (entry['function'] || "anonymous"), entry.file, entry.line); }).join('\n'); } else { formatted += 'not provided'; } } else { try { formatted += serialize(value); } catch (e) { try { formatted += serialize(value.toString()); } catch (e2) { formatted += '(unserializable value)'; } } } return formatted; } exports.formatTestValue = formatTestValue; /** * Retrieves the value of an Object foreign property using a dot-separated * path string. * * Beware, this function doesn't handle object key names containing a dot. * * @param Object obj The source object * @param String path Dot separated path, eg. "x.y.z" */ function getPropertyPath(obj, path) { "use strict"; if (!isObject(obj) || !isString(path)) { return undefined; } var value = obj; path.split('.').forEach(function(property) { if (typeof value === "object" && property in value) { value = value[property]; } else { value = undefined; } }); return value; } exports.getPropertyPath = getPropertyPath; /** * Indents a string. * * @param String string * @param Number nchars * @param String prefix * @return String */ function indent(string, nchars, prefix) { "use strict"; return string.split('\n').map(function(line) { return (prefix || '') + new Array(nchars).join(' ') + line; }).join('\n'); } exports.indent = indent; /** * Inherit the prototype methods from one constructor into another. * * @param {function} ctor Constructor function which needs to inherit the * prototype. * @param {function} superCtor Constructor function to inherit prototype from. */ function inherits(ctor, superCtor) { "use strict"; ctor.super_ = ctor.__super__ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); } exports.inherits = inherits; /** * Checks if value is a javascript Array * * @param mixed value * @return Boolean */ function isArray(value) { "use strict"; return Array.isArray(value) || isType(value, "array"); } exports.isArray = isArray; /** * Checks if passed argument is an instance of Capser object. * * @param mixed value * @return Boolean */ function isCasperObject(value) { "use strict"; return value instanceof require('casper').Casper; } exports.isCasperObject = isCasperObject; /** * Checks if value is a phantomjs clipRect-compatible object * * @param mixed value * @return Boolean */ function isClipRect(value) { "use strict"; return isType(value, "cliprect") || ( isObject(value) && isNumber(value.top) && isNumber(value.left) && isNumber(value.width) && isNumber(value.height) ); } exports.isClipRect = isClipRect; /** * Checks that the subject is falsy. * * @param Mixed subject Test subject * @return Boolean */ function isFalsy(subject) { "use strict"; /*eslint eqeqeq:0*/ return !subject; } exports.isFalsy = isFalsy; /** * Checks if value is a javascript Function * * @param mixed value * @return Boolean */ function isFunction(value) { "use strict"; return isType(value, "function"); } exports.isFunction = isFunction; /** * Checks if passed resource involves an HTTP url. * * @param Object resource The PhantomJS HTTP resource object * @return Boolean */ function isHTTPResource(resource) { "use strict"; return isObject(resource) && /^http/i.test(resource.url); } exports.isHTTPResource = isHTTPResource; /** * Checks if a file is apparently javascript compatible (.js or .coffee). * * @param String file Path to the file to test * @return Boolean */ function isJsFile(file) { "use strict"; var ext = fileExt(file); var valid = Object.keys(require.extensions).map(function(val) { return val.replace(/^\./, ''); }).filter(function(ext) { return ext === 'js' || ext === 'coffee'; }); return isString(ext, "string") && valid.indexOf(ext) !== -1; } exports.isJsFile = isJsFile; /** * Checks if the provided value is null * * @return Boolean */ function isNull(value) { "use strict"; return isType(value, "null"); } exports.isNull = isNull; /** * Checks if value is a javascript Number * * @param mixed value * @return Boolean */ function isNumber(value) { "use strict"; return isType(value, "number"); } exports.isNumber = isNumber; /** * Checks if value is a javascript Object * * @param mixed value * @return Boolean */ function isObject(value) { "use strict"; var objectTypes = ["array", "object", "qtruntimeobject"]; return objectTypes.indexOf(betterTypeOf(value)) >= 0; } exports.isObject = isObject; /** * Checks if value is a RegExp * * @param mixed value * @return Boolean */ function isRegExp(value) { "use strict"; return isType(value, "regexp"); } exports.isRegExp = isRegExp; /** * Checks if value is a javascript String * * @param mixed value * @return Boolean */ function isString(value) { "use strict"; return isType(value, "string"); } exports.isString = isString; /** * Checks that the subject is truthy. * * @param Mixed subject Test subject * @return Boolean */ function isTruthy(subject) { "use strict"; /*eslint eqeqeq:0*/ return !!subject; } exports.isTruthy = isTruthy; /** * Shorthands for checking if a value is of the given type. Can check for * arrays. * * @param mixed what The value to check * @param String typeName The type name ("string", "number", "function", etc.) * @return Boolean */ function isType(what, typeName) { "use strict"; if (typeof typeName !== "string" || !typeName) { throw new CasperError("You must pass isType() a typeName string"); } return betterTypeOf(what).toLowerCase() === typeName.toLowerCase(); } exports.isType = isType; /** * Checks if the provided value is undefined * * @return Boolean */ function isUndefined(value) { "use strict"; return isType(value, "undefined"); } exports.isUndefined = isUndefined; /** * Checks if value is a valid selector Object. * * @param mixed value * @return Boolean */ function isValidSelector(value) { "use strict"; if (isString(value)) { try { // phantomjs env has a working document object, let's use it document.querySelector(value); } catch(e) { if ('name' in e && (e.name === 'SYNTAX_ERR' || e.name === 'SyntaxError')) { return false; } } return true; } else if (isObject(value)) { if (!value.hasOwnProperty('type')) { return false; } if (!value.hasOwnProperty('path')) { return false; } if (['css', 'xpath'].indexOf(value.type) === -1) { return false; } return true; } return false; } exports.isValidSelector = isValidSelector; /** * Checks if the provided var is a WebPage instance * * @param mixed what * @return Boolean */ function isWebPage(what) { "use strict"; return betterTypeOf(what) === "qtruntimeobject" && what.objectName === 'WebPage'; } exports.isWebPage = isWebPage; function isPlainObject(obj) { "use strict"; if (!obj || typeof(obj) !== 'object') return false; var type = Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); return (type === 'object'); } /** * Object recursive merging utility for use in the SlimerJS environment * * @param Object origin the origin object * @param Object add the object to merge data into origin * @param Object opts optional options to be passed in * @return Object */ function mergeObjectsInGecko(origin, add, opts) { "use strict"; var options = opts || {}, keepReferences = options.keepReferences; for (var p in add) { if (isPlainObject(add[p])) { if (isPlainObject(origin[p])) { origin[p] = mergeObjects(origin[p], add[p]); } else { origin[p] = keepReferences ? add[p] : clone(add[p]); } } else { // if a property is only a getter, we could have a Javascript error // in strict mode "TypeError: setting a property that has only a getter" // when setting the value to the new object (gecko 25+). // To avoid it, let's define the property on the new object, do not set // directly the value var prop = Object.getOwnPropertyDescriptor(add, p); if (prop.get && !prop.set) { Object.defineProperty(origin, p, prop); } else { origin[p] = add[p]; } } } return origin; } /** * Object recursive merging utility. * * @param Object origin the origin object * @param Object add the object to merge data into origin * @param Object opts optional options to be passed in * @return Object */ function mergeObjects(origin, add, opts) { "use strict"; var options = opts || {}, keepReferences = options.keepReferences; if (phantom.casperEngine === 'slimerjs') { // Because of an issue in the module system of slimerjs (security membranes?) // constructor is undefined. // let's use an other algorithm return mergeObjectsInGecko(origin, add, options); } for (var p in add) { if (add[p] && add[p].constructor === Object) { if (origin[p] && origin[p].constructor === Object) { origin[p] = mergeObjects(origin[p], add[p]); } else { origin[p] = keepReferences ? add[p] : clone(add[p]); } } else { origin[p] = add[p]; } } return origin; } exports.mergeObjects = mergeObjects; /** * Converts milliseconds to seconds and rounds the results to 3 digits accuracy. * * @param Number milliseconds * @return Number seconds */ function ms2seconds(milliseconds) { "use strict"; return Math.round(milliseconds / 1000 * 1000) / 1000; } exports.ms2seconds = ms2seconds; /** * Creates an (SG|X)ML node element. * * @param String name The node name * @param Object attributes Optional attributes * @return HTMLElement */ function node(name, attributes) { "use strict"; var _node = document.createElementNS('', name); for (var attrName in attributes) { var value = attributes[attrName]; if (attributes.hasOwnProperty(attrName) && isString(attrName)) { _node.setAttribute(attrName, value); } } return _node; } exports.node = node; /** * Maps an object to an array made from its values. * * @param Object obj * @return Array */ function objectValues(obj) { "use strict"; return Object.keys(obj).map(function(arg) { return obj[arg]; }); } exports.objectValues = objectValues; /** * Prepares a string for xpath expression with the condition [text()=]. * * @param String string * @return String */ function quoteXPathAttributeString(string) { "use strict"; if (/"/g.test(string)) { return 'concat("' + string.toString().replace(/"/g, '", \'"\', "') + '")'; } else { return '"' + string + '"'; } } exports.quoteXPathAttributeString = quoteXPathAttributeString; /** * Serializes a value using JSON. * * @param Mixed value * @return String */ function serialize(value, indent) { "use strict"; if (isArray(value)) { value = value.map(function _map(prop) { return isFunction(prop) ? prop.toString().replace(/\s{2,}/, '') : prop; }); } return JSON.stringify(value, null, indent); } exports.serialize = serialize; /** * Returns unique values from an array. * * Note: ugly code is ugly, but efficient: http://jsperf.com/array-unique2/8 * * @param Array array * @return Array */ function unique(array) { "use strict"; var o = {}, r = []; for (var i = 0, len = array.length; i !== len; i++) { var d = array[i]; if (o[d] !== 1) { o[d] = 1; r[r.length] = d; } } return r; } exports.unique = unique; /** * Convert a version object to a string. * * @param Mixed version a version string or object */ function versionToString(version) { if (isObject(version)) { try { return [version.major, version.minor, version.patch].join('.'); } catch (e) {} } return version; } exports.versionToString = versionToString; /** * Compare two version numbers represented as strings. * * @param String a Version a * @param String b Version b * @return Number */ function cmpVersion(a, b) { "use strict"; var i, cmp, len, re = /(\.0)+[^\.]*$/; a = versionToString(a); b = versionToString(b); a = (a + '').replace(re, '').split('.'); b = (b + '').replace(re, '').split('.'); len = Math.min(a.length, b.length); for (i = 0; i < len; i++) { cmp = parseInt(a[i], 10) - parseInt(b[i], 10); if (cmp !== 0) { return cmp; } } return a.length - b.length; } exports.cmpVersion = cmpVersion; /** * Checks if a version number string is greater or equals another. * * @param String a Version a * @param String b Version b * @return Boolean */ function gteVersion(a, b) { "use strict"; return cmpVersion(a, b) >= 0; } exports.gteVersion = gteVersion; /** * Checks if a version number string is less than another. * * @param String a Version a * @param String b Version b * @return Boolean */ function ltVersion(a, b) { "use strict"; return cmpVersion(a, b) < 0; } exports.ltVersion = ltVersion; /** * Checks if the engine matches a specifier. * * A match specifier is an object of the form: * { * name: 'casperjs' | 'phantomjs', * version: { * min: Object, * max: Object * }, * message: String * } * * Minimal and maximal versions to be matched are determined using * utils.cmpVersion. * * @param Mixed matchSpec a single match specifier object or * an Array of match specifier objects * @return Boolean */ function matchEngine(matchSpec) { if (Array !== matchSpec.constructor) { matchSpec = [matchSpec]; } var idx; var len = matchSpec.length; var engineName = phantom.casperEngine; var engineVersion = phantom.version; for (idx = 0; idx < len; ++idx) { var match = matchSpec[idx]; var version = match.version; var min = version && version.min; var max = version && version.max; if ('*' === min) { min = null; } if ('*' === max) { max = null; } if (match.name === engineName && (!min || gteVersion(engineVersion, min)) && (!max || !ltVersion(max, engineVersion)) ) { return match; } } return false; } exports.matchEngine = matchEngine; webshot/inst/casperjs/modules/querystring.js0000644000176200001440000001307413030514600021113 0ustar liggesusers// Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. // Query String Utilities var QueryString = exports; // If obj.hasOwnProperty has been overridden, then calling // obj.hasOwnProperty(prop) will break. // See: https://github.com/joyent/node/issues/1707 function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function charCode(c) { return c.charCodeAt(0); } // a safe fast alternative to decodeURIComponent QueryString.unescapeBuffer = function(s, decodeSpaces) { var out = new Buffer(s.length); var state = 'CHAR'; // states: CHAR, HEX0, HEX1 var n, m, hexchar; for (var inIndex = 0, outIndex = 0; inIndex <= s.length; inIndex++) { var c = s.charCodeAt(inIndex); switch (state) { case 'CHAR': switch (c) { case charCode('%'): n = 0; m = 0; state = 'HEX0'; break; case charCode('+'): if (decodeSpaces) c = charCode(' '); // pass thru default: out[outIndex++] = c; break; } break; case 'HEX0': state = 'HEX1'; hexchar = c; if (charCode('0') <= c && c <= charCode('9')) { n = c - charCode('0'); } else if (charCode('a') <= c && c <= charCode('f')) { n = c - charCode('a') + 10; } else if (charCode('A') <= c && c <= charCode('F')) { n = c - charCode('A') + 10; } else { out[outIndex++] = charCode('%'); out[outIndex++] = c; state = 'CHAR'; break; } break; case 'HEX1': state = 'CHAR'; if (charCode('0') <= c && c <= charCode('9')) { m = c - charCode('0'); } else if (charCode('a') <= c && c <= charCode('f')) { m = c - charCode('a') + 10; } else if (charCode('A') <= c && c <= charCode('F')) { m = c - charCode('A') + 10; } else { out[outIndex++] = charCode('%'); out[outIndex++] = hexchar; out[outIndex++] = c; break; } out[outIndex++] = 16 * n + m; break; } } // TODO support returning arbitrary buffers. return out.slice(0, outIndex - 1); }; QueryString.unescape = function(s, decodeSpaces) { return QueryString.unescapeBuffer(s, decodeSpaces).toString(); }; QueryString.escape = function(str) { return encodeURIComponent(str); }; var stringifyPrimitive = function(v) { switch (typeof v) { case 'string': return v; case 'boolean': return v ? 'true' : 'false'; case 'number': return isFinite(v) ? v : ''; default: return ''; } }; QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) { sep = sep || '&'; eq = eq || '='; if (obj === null) { obj = undefined; } if (typeof obj === 'object') { return Object.keys(obj).map(function(k) { var ks = QueryString.escape(stringifyPrimitive(k)) + eq; if (Array.isArray(obj[k])) { return obj[k].map(function(v) { return ks + QueryString.escape(stringifyPrimitive(v)); }).join(sep); } else { return ks + QueryString.escape(stringifyPrimitive(obj[k])); } }).join(sep); } if (!name) return ''; return QueryString.escape(stringifyPrimitive(name)) + eq + QueryString.escape(stringifyPrimitive(obj)); }; // Parse a key=val string. QueryString.parse = QueryString.decode = function(qs, sep, eq, options) { sep = sep || '&'; eq = eq || '='; var obj = {}; if (typeof qs !== 'string' || qs.length === 0) { return obj; } var regexp = /\+/g; qs = qs.split(sep); var maxKeys = 1000; if (options && typeof options.maxKeys === 'number') { maxKeys = options.maxKeys; } var len = qs.length; // maxKeys <= 0 means that we should not limit keys count if (maxKeys > 0 && len > maxKeys) { len = maxKeys; } for (var i = 0; i < len; ++i) { var x = qs[i].replace(regexp, '%20'), idx = x.indexOf(eq), kstr, vstr, k, v; if (idx >= 0) { kstr = x.substr(0, idx); vstr = x.substr(idx + 1); } else { kstr = x; vstr = ''; } try { k = decodeURIComponent(kstr); v = decodeURIComponent(vstr); } catch (e) { k = QueryString.unescape(kstr, true); v = QueryString.unescape(vstr, true); } if (!hasOwnProperty(obj, k)) { obj[k] = v; } else if (Array.isArray(obj[k])) { obj[k].push(v); } else { obj[k] = [obj[k], v]; } } return obj; }; webshot/inst/casperjs/bin/0000755000176200001440000000000014225324365015272 5ustar liggesuserswebshot/inst/casperjs/bin/bootstrap.js0000755000176200001440000003641514225324365017661 0ustar liggesusers/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/n1k0/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*eslint max-statements:0, complexity:0*/ // node check if ('process' in this && this.process.title === "node") { console.error('CasperJS cannot be executed within a nodejs environment'); this.process.exit(1); } // phantom check if (!('phantom' in this)) { console.error('CasperJS needs to be executed in a PhantomJS environment https://phantomjs.org/'); } // Common polyfills // cujos bind shim instead of MDN shim, see #1396 var isFunction = function(o) { return 'function' === typeof o; }; var bind; var slice = [].slice; var proto = Function.prototype; var featureMap = { 'function-bind': 'bind' }; function has(feature) { var prop = featureMap[feature]; return isFunction(proto[prop]); } // check for missing features if (!has('function-bind')) { // adapted from Mozilla Developer Network example at // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind bind = function bind(obj) { var args = slice.call(arguments, 1), self = this, nop = function() { }, bound = function() { return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))); }; nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined bound.prototype = new nop(); return bound; }; proto.bind = bind; } // Custom base error var CasperError = function CasperError(msg) { "use strict"; Error.call(this); this.message = msg; this.name = 'CasperError'; }; CasperError.prototype = Object.getPrototypeOf(new Error()); // casperjs env initialization (function(global, phantom, system){ "use strict"; // phantom args var phantomArgs = system.args.slice(1); if ("slimer" in global) { phantom.casperEngine = "slimerjs"; } else { phantom.casperEngine = "phantomjs"; } if (phantom.casperLoaded) { return; } function __exit(statusCode){ setTimeout(function() { phantom.exit(statusCode); }, 0); } function __die(message) { if (message) { console.error(message); } __exit(1); } function __terminate(message) { if (message) { console.log(message); } __exit(); } (function (version) { // required version check if (phantom.casperEngine === 'phantomjs') { if (version.major === 1) { if (version.minor < 9) { return __die('CasperJS needs at least PhantomJS v1.9 or later.'); } if (version.minor === 9 && version.patch < 1) { return __die('CasperJS needs at least PhantomJS v1.9.1 or later.'); } } else if (version.major === 2) { // No requirements yet known } else { return __die('CasperJS needs PhantomJS v1.9.x or v2.x'); } } })(phantom.version); // Hooks in default phantomjs error handler phantom.onError = function onPhantomError(msg, trace) { phantom.defaultErrorHandler.apply(phantom, arguments); // print a hint when a possible casperjs command misuse is detected if (msg.indexOf("ReferenceError: Can't find variable: casper") === 0) { console.error('Hint: you may want to use the `casperjs test` command.'); } // exits on syntax error if (msg.indexOf('SyntaxError: ') === 0) { __die(); } }; // Patching fs var fs = (function patchFs(fs) { if (!fs.hasOwnProperty('basename')) { fs.basename = function basename(path) { return path.replace(/.*\//, ''); }; } if (!fs.hasOwnProperty('dirname')) { fs.dirname = function dirname(path) { if (!path) return undefined; return path.toString().replace(/\\/g, '/').replace(/\/[^\/]*$/, ''); }; } if (!fs.hasOwnProperty('isWindows')) { fs.isWindows = function isWindows() { var testPath = arguments[0] || this.workingDirectory; return (/^[a-z]{1,2}:/i).test(testPath) || testPath.indexOf("\\\\") === 0; }; } if (fs.hasOwnProperty('joinPath')) { fs.pathJoin = fs.joinPath; } else if (!fs.hasOwnProperty('pathJoin')) { fs.pathJoin = function pathJoin() { return Array.prototype.join.call(arguments, '/'); }; } return fs; })(require('fs')); // CasperJS root path if (!phantom.casperPath) { try { phantom.casperPath = phantomArgs.map(function _map(arg) { var match = arg.match(/^--casper-path=(.*)/); if (match) { return fs.absolute(match[1]); } }).filter(function _filter(path) { return fs.isDirectory(path); }).pop(); } catch (e) { return __die("Couldn't find nor compute phantom.casperPath, exiting."); } } /** * Prints CasperJS help. */ function printHelp() { /* global slimer */ var engine = phantom.casperEngine === 'slimerjs' ? slimer : phantom; var version = [engine.version.major, engine.version.minor, engine.version.patch].join('.'); return __terminate([ 'CasperJS version ' + phantom.casperVersion.toString() + ' at ' + phantom.casperPath + ', using ' + phantom.casperEngine + ' version ' + version, fs.read(fs.pathJoin(phantom.casperPath, 'bin', 'usage.txt')) ].join('\n')); } /** * Patched require to allow loading of native casperjs modules. * Every casperjs module have to first call this function in order to * load a native casperjs module: * * var require = patchRequire(require); * var utils = require('utils'); * * Useless for SlimerJS */ function patchRequire(require) { if (require.patched) { return require; } function fromPackageJson(module, dir) { var pkgPath, pkgContents, pkg; pkgPath = fs.pathJoin(dir, module, 'package.json'); if (!fs.exists(pkgPath)) { return; } pkgContents = fs.read(pkgPath); if (!pkgContents) { return; } try { pkg = JSON.parse(pkgContents); } catch (e) { return; } if (typeof pkg === "object" && pkg.main) { return fs.absolute(fs.pathJoin(dir, module, pkg.main)); } } function resolveFile(path, dir) { var extensions = ['js', 'coffee', 'json']; var basenames = [path, path + '/index']; var paths = []; var nodejsScript = fromPackageJson(path, dir); if (nodejsScript) { return nodejsScript; } basenames.forEach(function(basename) { paths.push(fs.absolute(fs.pathJoin(dir, basename))); extensions.forEach(function(extension) { paths.push(fs.absolute(fs.pathJoin(dir, [basename, extension].join('.')))); }); }); for (var i = 0; i < paths.length; i++) { if (fs.isFile(paths[i])) { return paths[i]; } } return null; } function getCurrentScriptRoot() { if ((phantom.casperScriptBaseDir || "").indexOf(fs.workingDirectory) === 0) { return phantom.casperScriptBaseDir; } return fs.absolute(fs.pathJoin(fs.workingDirectory, phantom.casperScriptBaseDir)); } function casperBuiltinPath(path) { return resolveFile(path, fs.pathJoin(phantom.casperPath, 'modules')); } function nodeModulePath(path) { var resolved, prevBaseDir; var baseDir = getCurrentScriptRoot(); do { resolved = resolveFile(path, fs.pathJoin(baseDir, 'node_modules')); prevBaseDir = baseDir; baseDir = fs.absolute(fs.pathJoin(prevBaseDir, '..')); } while (!resolved && baseDir !== '/' && baseDir !== prevBaseDir); return resolved; } function localModulePath(path) { return resolveFile(path, phantom.casperScriptBaseDir || fs.workingDirectory); } var patchedRequire = function patchedRequire(path) { try { return require(casperBuiltinPath(path) || nodeModulePath(path) || localModulePath(path) || path); } catch (e) { throw new CasperError("Can't find module " + path); } }; patchedRequire.cache = require.cache; patchedRequire.extensions = require.extensions; patchedRequire.stubs = require.stubs; patchedRequire.patched = true; return patchedRequire; } /** * Initializes the CasperJS Command Line Interface. */ function initCasperCli(casperArgs) { /*eslint complexity:0*/ var baseTestsPath = fs.pathJoin(phantom.casperPath, 'tests'); function setScriptBaseDir(scriptName) { var dir = fs.dirname(scriptName); if (dir === scriptName) { dir = '.'; } phantom.casperScriptBaseDir = dir; } if (!!casperArgs.options.version) { return __terminate(phantom.casperVersion.toString()); } else if (casperArgs.get(0) === "test") { phantom.casperScript = fs.absolute(fs.pathJoin(baseTestsPath, 'run.js')); phantom.casperTest = true; casperArgs.drop("test"); setScriptBaseDir(casperArgs.get(0)); } else if (casperArgs.get(0) === "selftest") { phantom.casperScript = fs.absolute(fs.pathJoin(baseTestsPath, 'run.js')); phantom.casperSelfTest = phantom.casperTest = true; casperArgs.options.includes = fs.pathJoin(baseTestsPath, 'selftest.js'); if (casperArgs.args.length <= 1) { casperArgs.args.push(fs.pathJoin(baseTestsPath, 'suites')); } casperArgs.drop("selftest"); phantom.casperScriptBaseDir = fs.dirname(casperArgs.get(1) || fs.dirname(phantom.casperScript)); } else if (casperArgs.args.length === 0 || !!casperArgs.options.help) { return printHelp(); } if (!phantom.casperScript) { phantom.casperScript = casperArgs.get(0); } if (phantom.casperScript !== "/dev/stdin" && !fs.isFile(phantom.casperScript)) { return __die('Unable to open file: ' + phantom.casperScript); } if (!phantom.casperScriptBaseDir) { setScriptBaseDir(phantom.casperScript); } // filter out the called script name from casper args casperArgs.drop(phantom.casperScript); } // CasperJS version, extracted from package.json - see http://semver.org/ phantom.casperVersion = (function getCasperVersion(path) { var parts, patchPart, pkg, pkgFile; pkgFile = fs.absolute(fs.pathJoin(path, 'package.json')); if (!fs.exists(pkgFile)) { throw new CasperError('Cannot find package.json at ' + pkgFile); } try { pkg = JSON.parse(require('fs').read(pkgFile)); } catch (e) { throw new CasperError('Cannot read package file contents: ' + e); } parts = pkg.version.trim().split("."); if (parts.length < 3) { throw new CasperError("Invalid version number"); } patchPart = parts[2].split('-'); return { major: ~~parts[0] || 0, minor: ~~parts[1] || 0, patch: ~~patchPart[0] || 0, ident: patchPart[1] || "", toString: function toString() { var version = [this.major, this.minor, this.patch].join('.'); if (this.ident) { version = [version, this.ident].join('-'); } return version; } }; })(phantom.casperPath); // phantomjs2 has paths in require, but needs patchRequire anyway if (!("paths" in global.require) || ('phantomjs' === phantom.casperEngine && 1 < phantom.version.major) ) { global.__require = require; global.patchRequire = patchRequire; // must be called in every casperjs module as of 1.1 global.require = patchRequire(global.require); } else { // declare a dummy patchRequire function global.patchRequire = function(req) {return req;}; require.paths.push(fs.pathJoin(phantom.casperPath, 'modules')); require.paths.push(fs.workingDirectory); } if (phantom.casperEngine === 'slimerjs') { require.globals.patchRequire = global.patchRequire; require.globals.CasperError = CasperError; } // casper cli args phantom.casperArgs = require('cli').parse(phantomArgs); if (true === phantom.casperArgs.get('cli')) { initCasperCli(phantom.casperArgs); } if ("paths" in global.require) { if ((phantom.casperScriptBaseDir || "").indexOf(fs.workingDirectory) === 0) { require.paths.push(phantom.casperScriptBaseDir); } else { require.paths.push(fs.pathJoin(fs.workingDirectory, phantom.casperScriptBaseDir)); } require.paths.push(fs.pathJoin(require.paths[require.paths.length-1], 'node_modules')); } // casper loading status flag phantom.casperLoaded = true; // passed casperjs script execution if (phantom.casperScript && !phantom.injectJs(phantom.casperScript)) { return __die('Unable to load script ' + phantom.casperScript + '; check file syntax'); } })(this, phantom, require('system')); webshot/inst/casperjs/package.json0000755000176200001440000000131313030514600016773 0ustar liggesusers{ "name": "casperjs", "description": "A navigation scripting & testing utility for PhantomJS and SlimerJS", "version": "1.1.0-beta5", "keywords": [ "phantomjs", "slimerjs", "test", "testing", "scraping" ], "bin": "./bin/casperjs", "author": { "name": "Nicolas Perriault", "email": "nicolas@perriault.net", "web": "https://nicolas.perriault.net/" }, "bugs": { "url": "https://github.com/n1k0/casperjs/issues" }, "repository": { "type": "git", "url": "git://github.com/n1k0/casperjs.git" }, "licenses": [ { "type": "MIT", "url": "http://www.opensource.org/licenses/mit-license.php" } ], "homepage": "http://casperjs.org" } webshot/inst/utils.js0000644000176200001440000000132113030514600014365 0ustar liggesusers// Given an array of arguments like: // [ '--vwidth=800','--vheight=600','--cliprect=0,0,800,600' ] // return an object like: // { vwidth: '800', vheight: '600', cliprect: '0,0,800,600' } exports.parseArgs = function(args) { opts = {}; args.forEach(function(arg) { arg = arg.replace(/^--/, ""); var eq_idx = arg.indexOf("="); var argname = arg.substring(0, eq_idx); var argvalue = arg.substring(eq_idx + 1); opts[argname] = argvalue; }); return opts; }; // Copy properties from object b that are not defined in object a into object a. exports.fillMissing = function(a, b) { for (var i in b) { if (b.hasOwnProperty(i) && !a.hasOwnProperty(i)) a[i] = b[i]; } return a; };