commit 0a624cd9a73c2afee9131c364c700e293834aeff Author: Dmitry Smirnov Date: Thu Jun 16 09:26:25 2016 +0200 phantomjs (2.1.1+dfsg-2) unstable; urgency=medium * New patch to allow comilation with QT versions other than 5.5 (Closes: #827421). * Run tests under "dbus-launch"; Build-Depends += "dbus-x11". * Respect DEB_BUILD_OPTIONS="nocheck". * README: added note about lack of headless functionality. * Corrected short package description (Closes: #826541). Thanks, Mario Lang. * Standards-Version: 3.9.8. * Corrected Vcs-Git URL. # imported from the archive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb7857e --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +.DS_Store +*.pro.user* +*.xcodeproj +Makefile* +*~ +*.moc +moc_* +qrc_* +.qmake.stash +*.o +*.swp +*.pyc +*.a +/deploy/qt-*.tar.gz +/deploy/Qt-* +/symbols +/src/qt/qtc-debugging-helper +/src/phantomjs_plugin_import.cpp + +# ignore ctags +/tags +/tools/dump_syms.app/ + +# Ignore Visual Studio temporary files, build results, etc +*.suo +*.user +*.sln.docstates +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.sdf +*.vcxproj +*.vcxproj.filters +*.lib +*.prl +*.intermediate.manifest + +# Build results +[Dd]ebug*/ +[Rr]elease/ +bin/ +*.class +build/ +.gradle/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..17c015d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,11 @@ +[submodule "qtbase"] + path = src/qt/qtbase + url = https://github.com/Vitallium/qtbase.git + branch = phantomjs +[submodule "qtwebkit"] + path = src/qt/qtwebkit + url = https://github.com/Vitallium/qtwebkit.git + branch = phantomjs +[submodule "3rdparty-win"] + path = src/qt/3rdparty + url = https://github.com/Vitallium/phantomjs-3rdparty-win.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7fd2f83 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,87 @@ +# Contribution Guide + +This page describes how to contribute changes to PhantomJS. + +Please do **not** create a pull request without reading this guide first. Failure to do so may result in the **rejection** of the pull request. + +## For The Impatients + +**Work on a feature branch**. +If your changes need to be modified due to some reviews, it is less clutter to tweak an isolated feature branch and push it again. + +**Create a ticket in the issue tracker**. +This serves as a placeholder for important feedback, review, or any future updates. + +In the commit message: + +* **Keep the first line < 72 characters**. Write additional paragraphs + if necessary. +* **Put the link to the issue** (see above). This is important for cross-referencing purposes. + +## Communicate + +*Second opinion is always important.* + +**Bug fixing**. If you have a fix for a bug, please attach your patch in the corresponding issue in the [issue tracker](https://github.com/ariya/phantomjs/issues). If there is no entry for the bug yet, then please create a new one. If you are confident working with Git, see the Get Ready section below on how to submit your change. + +**Improvement and feature request**. If you have an improvement idea, please send an email to the [mailing list](http://groups.google.com/group/phantomjs) (rather than contacting the developers directly) so that other people can give their insights and opinions. This is also important to avoid duplicate work. + +**Task management**. Once the feature idea is agreed upon and translated into concrete actions and tasks, please use the [issue tracker](https://github.com/ariya/phantomjs/issues) to create an issue for each individual task. Further technical discussion about the task and the implementation details should be carried out in the issue tracker. + +**Extending with new API**. Whenever you want to introduce a new API, please send an email to the mailing list along with the link to the issue. Consider good API name for the object or function, read the [API Design Principle](http://wiki.qt.io/API_Design_Principles) article. It may require few iterations to agree on the final API and hence it is important to engage all interested parties as early as possible. + +## Get Ready + +For your proposed change, you need to have: + +* **an issue** (in the issue tracker) which describe your bug or feature +* **a feature branch** in your git fork + +### Refer the Issue + +The commit message needs to link to the issue. This cross-reference is [very important](http://ariya.ofilabs.com/2012/01/small-scale-software-craftsmanship.html) for the following reasons. + +First, the commit log is frozen and can not be changed. If it contains a mistake or outdated information, the log can not be amended. However, further updates can be still posted to the linked issue, which can be followed from the commit log itself. + +Second, it provides a placeholder for code review and other feedback. + +An example of a bad commit log: + + Fix Mountain Lion + +The above log is too short and useless in the long run. A better version (and note the issue link): + + Better support for OS X Mountain Lion. + + require('system').os.version should give "10.8 (Mountain Lion)". + + https://github.com/ariya/phantomjs/issues/10688 + +### Use Feature Branch + +To isolate your change, please avoid working on the master branch. Instead, work on a *feature branch* (often also known as *topic branch*). You can create a new branch (example here crash-fix) off the master branch by using: + + git checkout -b crash-fix master + +Refer to your favorite Git tutorial/book for further detailed help. + +Some good practices for the feature branch: + +* Give it a meaningful name instead of, e.g. `prevent-zero-divide` instead of just `fix` +* Make *granular* and *atomic* commits, e.g. do not mix a typo fix with some major refactoring +* Keep one branch for one specific issue. If you need to work on other unrelated issues, create another branch. + +## Review and Merge + +When your branch is ready, send the pull request. + +While it is not always the case, often it is necessary to improve parts of your code in the branch. This is the actual review process. + +Here is a check list for the review: + +* It does not break the test suite +* There is no typo +* The coding style follows the existing one +* There is a reasonable amount of comment +* The license header is intact +* All examples are still working diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..a15f835 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,401 @@ +Please see also http://phantomjs.org/releases.html. + +2016-01-23: Version 2.1.0 + + New features + + * Upgraded Qt to 5.5.1 (issue #13377) + * Added support for SSL Client Authentication (issue #11275) + * Added support for context menu event (issue #11429) + * Allow remote debugging to use random port assigned by the OS (issue #13432) + + Improvements + + * Allow outer context to access arbitrary URLs (issue #11217) + * Fixed --local-storage-path and localStoragePath config option (issue #11596) + * Restored --local-url-access=no regression (issue #13412) + * Fixed an issue with loading JS modules contains a last-line comment (issue #12868) + * Fixed an issue with returning binary content in WebServer module (issue #13026) + * Fixed encoded URL loading on Windows (issue #12953) + * Fixed building with GCC 5 (issue #13518) + * Fixed file upload (issue #12506) + * Fixed latest OS detection (issue #13829) + +2015-01-23: Version 2.0.0 + + New features + + * Switched to Qt 5 and updated WebKit (issue 10448) + * Implemented clearing of memory cache (issue 10357) + * Added support for HTTP header change for every request (issue 11299) + + Improvements + + * Fixed rendering of CJK text by always linking the codecs (issue 10249) + * Ensured onResourceReceived is still fired on an error (issue 11163) + * Fixed possible crash in handling network requests (issue 11252) + * Removed hardcoded GhostDriver launching message (issue 12681) + * Allowed disk cache more than 2 GB (issue 12303) + + Examples + + * Netsniff example should exit when fails to load (issue 11333) + +2014-01-25: Version 1.9.7 + + * Reverted to GhostDriver 1.1.0 instead of 1.1.1 (issue 11915) + * Fixed another warning of obsolete userSpaceScaleFactor on OS X 10.9 (issue 11612) + +2014-01-20: Version 1.9.6 + + * Updated GhostDriver to version 1.1.1 (issue 11877, 11893) + +2014-01-19: Version 1.9.3 + + * Fixed CoreText performance note on OS X 10.9 (issue 11418) + * Fixed warning of obsolete userSpaceScaleFactor on OS X 10.9 (issue 11612) + +2013-09-06: Version 1.9.2 + + * Fixed graphical artifacts with transparent background on Windows (issue 11276, 11007, 11366) + * Updated GhostDriver to version 1.0.4 (issue 11452) + +2013-06-04: Version 1.9.1 + + Critical bug fixes: + + * Fixed problems with specifying proxy server (issue 10811, 11117) + * Fixed UTF-8 encoding with system.stdout and system.stderr (issue 11162) + * Ensured that onResourceReceived will be always invoked (issue 11163) + * Fixed module loading from an absolute path on Windows (issue 11165) + * Fixed typo in the command-line option for setting the cache size (11219) + * Fixed possible crash when handling network requests (issue 11252, 11338) + +2013-03-20: Version 1.9.0 "Sakura" + + New features + + * Added spawn and execFile to execute external programs (issue 10219) + * Added the ability to abort network requests (issue 10230) + * Added system access to stdin, stdout, and stderr (issue 10333) + * Added support for custom CA certificates location (issue 10916) + * Added seek function to the File stream (issue 10937) + * Implemented file read for a specified number of bytes (issue 10938) + * Added a callback to handle network error (issue 10954, 10997) + * Added custom encoding support when opening a page (issue 11043) + * Implemented require.stub() support for a factory function (issue 11044) + * Added page loading indicator and progress (issue 11091) + * Added a timeout option for network requests (issue 11129) + + Improvements + + * Fixed the build on FreeBSD (issue 10597) + * Ensured a consistent 72 dpi for Linux headless rendering (issue 10659) + * Fixed possible PDF error due to invalid CreationDate field (issue 10663) + * Fixed crash when uploading non existing files (issue 10941) + * Improved the autocomplete internal of the interactive/REPL mode (issue 10943) + * Fixed possible crash when accessing inline frames (issue 10947) + * Changed Linux binary package setup to be built on CentOS 5 (issue 10963) + * Extended SSL ignore setting to synchronous XHR (issue 10985) + * Added convenient constants for modifier keys (issue 11056) + * Fixed incorrect date handling in the cookies (issue 11068) + * Updated GhostDriver to version 1.0.3 (issue 11146) + + Examples + + * Fixed invalid data URI in the netsniff example (issue 10740) + * Implemented a new weather example (issue 10794) + * Fixed rendering issues in render_multi_url (issue 11021) + * Fixed proper event sequence in page_events example (issue 11028) + * Miscellanous tweaks (issue 11082) + +2013-03-02: Version 1.8.2 + + Critical bug fixes: + + * Fixed possible PDF error due to invalid CreationDate field (issue 663) + * Fixed crash when uploading non existing files (issue 941) + * Fixed possible crash when accessing inline frames (issue 947) + * Extended SSL ignore setting to synchronous XHR (issue 985) + * Fixed incorrect date handling in the cookies (issue 1068) + +2013-01-06: Version 1.8.1 + + Critical bug fix: + + * Mac OS X: Fix possible crash when using some TrueType fonts (issue 690) + +2012-12-21: Version 1.8.0 "Blue Winter Rose" + + New features + + * Integrated GhostDriver as the WebDriver implementation (issue 49) + * Added an option to specify the SSL protocol (issue 174) + * Added encoding support for WebServer's response (issue 505) + * Added process ID (PID) to the System module (issue 769) + * Added properties to obtain page and frame title (issue 799) + * Added page navigation methods (issue 808) + * Added support for modifier keys in keyboard events (issue 835) + * Added onFilePicker callback for more generic file upload API (issue 843) + * Added the ability to set the page content and location (issue 909) + + Improvements + + * Fixed date parsing in ISO8601 format (issue 187, 267) + * Fixed window.location (issue 530, 632) + * Deregistered multiple callback handler (issue 807) + * Fixed sending of double-click events (issue 848) + * Increases maximum number of redirects (issue 849) + * Fixed keycodes sent for lowercase characters (issue 852) + * Fixed a regression in table row page break (issue 880) + * Completed the CoffeeScript version of the examples (issue 907) + * Updated Qt to version 4.8.4 (issue 918) + * Fixed potential hang in some example scripts (issue 922) + +2012-09-22: Version 1.7.0 "Blazing Star" + + New features + + * Added a module system modelled after CommonJS/Node.js (issue 47) + * Added support for window pop-up (issue 151) + * Static build on Linux (issue 413) + * Added run-time detection of SSL support (issue 484) + * Added more events support (issue 492, 712) + * Added support for disabling automatic proxy detection (issue 580) + * Provided page closing callback (issue 678) + * Added methods to access URL, frames URL, frame Content (issue 758) + * Added more cookies-related API (issue 761) + + Improvements + + * Refactored command-line options handling (issue 55) + * Improved the workflow for producing release builds (issue 599) + * Improved cookies API and implementation (issue 603, 761) + * Improved frame switching API (issue 654) + * Fixed iframe handling regression (issue 683) + * Fixed OS version number with Windows 8 and Mountain Lion (issue 684, 688) + * Fixed HAR navigation info in the netsniff example (issue 733) + * Fixed compile warnings with Visual Studio (issue 744) + * Removed hacks for static linking on Windows (issue 753) + * Added ICO image handling on Windows (issue 779) + * Fixed font antialiasing on Windows (issue 785) + * Improved Jasmine test runner for Jasmine 1.2 (issue 792) + +2012-07-22: Version 1.6.1 + + Bug fixes + + * Don't build the deploy in debug mode (issue 599) + * Fixed building on Windows (issue 424) + * Fixed remote inspector when building statically (issue 430) + +2012-06-20: Version 1.6.0 "Lavender" + + New features + + * Added support for passing arguments to WebPage's evaluate (issue 132) + * Added callbacks for JavaScript onConfirm and onPrompt (issue 133) + * Added stack trace when error occurs (issue 166) + * Added support for local storage path and quota (issue 300) + * Added initial support for cookies handling (issue 354) + * Added support for header footer when printing the page (issue 410, 512) + * Added headers support in the loading request (issue 452) + * Added support to render the web page as base64-encoded string (issue 547) + * Added hooks for navigation event (issue 562) + * Added command-line option to show debug messages (issue 575) + * Added support for the zoom factor for web page rendering (issue 579) + * Added crash reporter for Mac OS X and Linux, based on Google Breakpad (issue 576) + * Added 'os' object to the system module (issue 585) + * Added support for asynchronous evaluation (issue 593) + + Improvements + + * Fixed remote debugging to work on Mac OS X and Windows (issue 430) + * Fixed web server getting the dropped connection for empty response (issue 451) + * Fixed text rendered as boxes (squares) on headless Linux (issue 460) + * Updated Qt to version 4.8.2 (issue 495) + * Updated CoffeeScript compiler to version 1.3.3 (issue 496) + * Fixed the build script to detect and use MAKEFLAGS (issue 503) + * Fixed the build script to properly pass Qt config flags (issue 507) + * Changed Info.plist to be embedded in Mac OS X executable (issue 528) + * Fixed wrong module require in the imagebin example (issue 536) + * Fixed example scripts to exit with the right exit code (issue 544) + * Fixed build failure with glib 2.31.0+ (issue 559) + * Fixed error handler failures in some cases (issue 589) + * Fixed Twitter-related examples to work with the new site (issue 609) + +2012-03-20: Version 1.5.0 "Ghost Flower" + + New features + + * Added interactive mode, also known as REPL (issue 252) + * Added setting for web security, to allow cross domain XHR (issue 28) + * Added error handler for WebPage object (issue 166) + * Added support for custom HTTP header in the network request (issue 77) + * Added support for read write encoding in the file system module (issue 367) + * Added remote debugging support on Linux (issue 6) + * Added support for proxy authentication (issue 105) + * Added System module, to retrieve environment variables (issue 271) and arguments (issue 276) + * Added fs.readLink function (issue 329) + * Added support for reading and writing binary data (issue 400) + * Added support to retrieve request data in the WebServer? module (issue 340) + * Added support for individual top/bottom/left/right print margins (issue 388) + * Added command-line option --help (issue 347) + * Added short command-line options -v and -h (issue 408) + * Removed support for Flash and other plugins (issue 418) + + Bug fixes + + * Fixed multiple console.log arguments (issue 36) + * Fixed file upload (issue 307) + * Fixed the web server instance to be asynchronous (issue 326) and still support Keep Alive (issue 416) + * Workaround Qt 4.8.0 crash due to empty URL scheme (issue 365) + * Fixed a Content-Type problem where POST does not work (issue 337) + * Fixed reading body request in the web server even without specific Content-Type (issue 439) + * Fixed Jasmine test runner with Jasmine 1.1 (issue 402) + * Fixed request URL formatting in the web server (issue 437) + * Don't display debugging and warning messages (issue 323) + +2011-12-31: Version 1.4.1 + + Bug fixes + + * Fix setting the proxy type (issue 266) + * Workaround for file upload regression (issue 307) + * Fix extraneous messsages in non-debug mode (issue 323) + +2011-12-22: Version 1.4.0 "Glory of the Snow" + + New features + + * Added embedded HTTP server (issue 115) + * Added convenient build script for Linux (issue 197) + * Added support for SOCKS5 proxy (issue 266) + * Updated CoffeeScript compiler to version 1.2 (issue 312) + + Bug fixes + + * Fix potential crash in QUrl with Qt 4.8 (issue 304) + * Fix bug in CookieJar with QSettings and string (PyPhantomJS issue 10) + * Prevent showing the icon on Mac OS X Dock (issue 281) + + Examples + + * Added a new example to detect browsers sniffing (issue 263) + * Added HTTP server example (issue 115) + +2011-09-23: Version 1.3.0 "Water Lily" + + Bug fixes + + * Fixed open() and POST method, without specifying the finished handler + * Fixed script execution warning dialog (issue 165) + * Added WebPage.release() to free the web page from memory (issue 154) + * Added special handling of about:blank (issue 235) + * Made a separate network access manager for each page (issue 190) + + New features + + * Introduced file system API based on CommonJS Filesystem proposal (issue 129) + * Added support for persistent cookies (issue 91) + * Added event handling, currently only for mouse events (issue 234) + * Added page scroll position (issue 162) + * Added HTTP authentication support (issue 45) + * Added callback for page initialization (issue 143) + * Added support to specify script and output encoding (issue 186) + * Added option to allow local content to do cross-domain access (issue 28) + * Added support to apply configurations from a JSON file (issue 180) + * Added a convenient WebPage initialization construction (issue 206) + * Added option to limit the size of disk cache (issue 220) + + Examples + + * Added a new example on using Modernizr to detect features (issue 144) + * Fixed pizza.js example to use Mobile Yelp (issue 200) + * Fixed netsniff.coffee example due to wrong indentation (issue 225) + * Added an example to show live network traffic (issue 227) + * Added an example demonstrating different output encodings (issue 186) + +2011-06-21: Version 1.2.0 "Birds of Paradise" + + Version 1.2.0 is a major update. It introduces a whole set of new API. + + Bug fixes + + * Fixed rendering a very large web page (issue 54) + * Fixed reporting of CoffeeScript compile error (issue 125) + + New features + + * Added callback for console message (issue 12) + * Improved security model via WebPage object (issue 41) + * Added support for POST, HEAD, PUT, and DELETE (issue 88) + * Scripts filename is now passed as phantom.scriptName + * Added callback to capture resource requests and responses (issue 2) + * Added the ability to load external JavaScript (issue 32) + + Examples + + * Ported examples to use WebPage object + * Added a new example to upload an image to imagebin.org + * Added a new example to show HTTP POST feature + * Added a new example to sniff network traffic and save it in HAR format + + +2011-04-27: Version 1.1.0 "Cherry Blossom" + + Fixed the script loading to use UTF-8 encoding (Yasuhiro Matsumoto). + + Added check for system proxy setting (Yasuhiro Matsumoto). + + Fixed building with Cygwin and Qt 4.5 (John Dalton). + + Added a new example: driver for QUnit tests (Łukasz Korecki). + + Fixed issue #20: problem with JPG transparent color (Alessandro Portale). + + Fixed issue #9: ignore first line starting with #! (Matthias, aka fourplusone). + + Fixed issue #7: support for file upload for form submission (Matthias, aka fourplusone). + + Fixed issue #35: support for disabling images loading (Ariya Hidayat). + + Fixed issue #14: enable or disable plugins (Ariya Hidayat). + + Added a new example: using Canvas to produce the color wheel (Ariya Hidayat). + + Added support for rasterizing as GIF image (Ariya Hidayat). + + Added support for CoffeeScript (Ariya Hidayat). + + Fixed issue #19: option for setting the proxy (Clint Berry, Ariya Hidayat). + + Python implementation using PyQt (James Roe). + + Fixed issue #17: Specify paper size for PDF export (Alessandro Portale). + + Fixed issue #60: Win32 and OS/2 icon files (Salvador Parra Camacho). + + Added clipping rectangle to the render function (Wouter de Bie). + + Added an example on sychronous waiting (Gabor Torok). + + Added command line option to use disk cache (Jon Turner). + + Added text extracting example (Weston Ruter). + + Fixed issue #93: Build with Qt < 4.7 (Ariya Hidayat). + + Ported all examples to CoffeeScript (Robert Gieseke). + +2011-01-17: Version 1.0.0 + + Initial launch. + + The API is centralized at the 'phantom' object (as child of + window object) which has the properties: args, content, + loadStatus, state, userAgent, version, viewportSize, and + the following functions: exit, open, render, sleep. + + Several examples are included, among others: web page rasterizer, + weather service, headless test framework driver, and many others. diff --git a/LICENSE.BSD b/LICENSE.BSD new file mode 100644 index 0000000..d5dfdd1 --- /dev/null +++ b/LICENSE.BSD @@ -0,0 +1,22 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c9578c --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# [PhantomJS](http://phantomjs.org) - Scriptable Headless WebKit + +PhantomJS ([phantomjs.org](http://phantomjs.org)) is a headless WebKit scriptable with JavaScript. The latest [stable release](http://phantomjs.org/release-2.0.html) is version 2.0. + +**Note**: Please **do not** create a GitHub pull request **without** reading the [Contribution Guide](https://github.com/ariya/phantomjs/blob/master/CONTRIBUTING.md) first. Failure to do so may result in the rejection of the pull request. + +## Use Cases + +- **Headless web testing**. Lightning-fast testing without the browser is now possible! +- **Page automation**. [Access and manipulate](http://phantomjs.org/page-automation.html) web pages with the standard DOM API, or with usual libraries like jQuery. +- **Screen capture**. Programmatically [capture web contents](http://phantomjs.org/screen-capture.html), including CSS, SVG and Canvas. Build server-side web graphics apps, from a screenshot service to a vector chart rasterizer. +- **Network monitoring**. Automate performance analysis, track [page loading](http://phantomjs.org/network-monitoring.html) and export as standard HAR format. + +## Features + +- **Multiplatform**, available on major operating systems: Windows, Mac OS X, Linux, and other Unices. +- **Fast and native implementation** of web standards: DOM, CSS, JavaScript, Canvas, and SVG. No emulation! +- **Pure headless (no X11) on Linux**, ideal for continuous integration systems. Also runs on Amazon EC2, Heroku, and Iron.io. +- **Easy to install**: [Download](http://phantomjs.org/download.html), unpack, and start having fun in just 5 minutes. + +## Questions? + +- Explore the complete [documentation](http://phantomjs.org/documentation/). +- Read tons of [user articles](http://phantomjs.org/buzz.html) on using PhantomJS. +- Join the [mailing-list](http://groups.google.com/group/phantomjs) and discuss with other PhantomJS fans. + +PhantomJS is free software/open source, and is distributed under the [BSD license](http://opensource.org/licenses/BSD-3-Clause). It contains third-party code, see the included `third-party.txt` file for the license information on third-party code. + +PhantomJS is created and maintained by [Ariya Hidayat](http://ariya.ofilabs.com/about) (Twitter: [@ariyahidayat](http://twitter.com/ariyahidayat)), with the help of [many contributors](https://github.com/ariya/phantomjs/contributors). Follow the official Twitter stream [@PhantomJS](http://twitter.com/PhantomJS) to get the frequent development updates. diff --git a/build.py b/build.py new file mode 100755 index 0000000..7fe7c5b --- /dev/null +++ b/build.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# This file is part of the PhantomJS project from Ofi Labs. +# +# Copyright (C) 2014 Milian Wolff, KDAB +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import argparse +import os +import platform +import sys +import subprocess +import re +import multiprocessing + +root = os.path.abspath(os.path.dirname(__file__)) +third_party_names = ["libicu", "libxml", "openssl", "zlib"] +third_party_path = os.path.join(root, "src", "qt", "3rdparty") + +openssl_search_paths = [{ + "name": "Brew", + "header": "/usr/local/opt/openssl/include/openssl/opensslv.h", + "flags": [ + "-I/usr/local/opt/openssl/include", + "-L/usr/local/opt/openssl/lib" + ] +}, { + "name": "MacPorts", + "header": "/opt/local/include/openssl/opensslv.h", + "flags": [ + "-I/opt/local/include", + "-L/opt/local/lib" + ] +}] + +# check if path points to an executable +# source: http://stackoverflow.com/a/377028 +def isExe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + +# find path to executable in PATH environment variable, similar to UNIX which command +# source: http://stackoverflow.com/a/377028 +def which(program): + if isExe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip("") + exe_file = os.path.join(path, program) + if isExe(exe_file): + return exe_file + return None + +# returns the path to the QMake executable which gets built in our internal QtBase fork +def qmakePath(): + exe = "qmake" + if platform.system() == "Windows": + exe += ".exe" + return os.path.abspath("/usr/bin/" + exe) + +# returns paths for 3rd party libraries (Windows only) +def findThirdPartyDeps(): + include_dirs = [] + lib_dirs = [] + for dep in third_party_names: + include_dirs.append("-I") + include_dirs.append(os.path.join(third_party_path, dep, "include")) + lib_dirs.append("-L") + lib_dirs.append(os.path.join(third_party_path, dep, "lib")) + return (include_dirs, lib_dirs) + +class PhantomJSBuilder(object): + options = {} + makeCommand = [] + + def __init__(self, options): + self.options = options + + # setup make command or equivalent with arguments + if platform.system() == "Windows": + makeExe = which("jom.exe") + if not makeExe: + makeExe = "nmake" + self.makeCommand = [makeExe, "/NOLOGO"] + else: + flags = [] + if self.options.jobs: + # number of jobs explicitly given + flags = ["-j", self.options.jobs] + elif not re.match("-j\s*[0-9]+", os.getenv("MAKEFLAGS", "")): + # if the MAKEFLAGS env var does not contain any "-j N", set a sane default + flags = ["-j", multiprocessing.cpu_count()] + self.makeCommand = ["make"] + self.makeCommand.extend(flags) + + # if there is no git subdirectory, automatically go into no-git + # mode + if not os.path.isdir(".git"): + self.options.skip_git = True + + # run the given command in the given working directory + def execute(self, command, workingDirectory): + # python 2 compatibility: manually convert to strings + command = [str(c) for c in command] + workingDirectory = os.path.abspath(workingDirectory) + print("Executing in %s: %s" % (workingDirectory, " ".join(command))) + if self.options.dry_run: + return 0 + process = subprocess.Popen(command, stdout=sys.stdout, stderr=sys.stderr, cwd=workingDirectory) + process.wait() + return process.returncode + + # run git clean in the specified path + def gitClean(self, path): + if self.options.skip_git: return 0 + return self.execute(["git", "clean", "-xfd"], path) + + # run make, nmake or jom in the specified path + def make(self, path): + return self.execute(self.makeCommand, path) + + # run qmake in the specified path + def qmake(self, path, options): + qmake = qmakePath() + # verify that qmake was properly built + if not isExe(qmake) and not self.options.dry_run: + raise RuntimeError("Could not find QMake executable: %s" % qmake) + command = [qmake] + if self.options.qmake_args: + command.extend(self.options.qmake_args) + if options: + command.extend(options) + return self.execute(command, path) + + # returns a list of platform specific Qt Base configure options + def platformQtConfigureOptions(self): + platformOptions = [] + if platform.system() == "Windows": + platformOptions = [ + "-mp", + "-static-runtime", + "-no-cetest", + "-no-angle", + "-icu", + "-openssl", + "-openssl-linked", + ] + deps = findThirdPartyDeps() + platformOptions.extend(deps[0]) + platformOptions.extend(deps[1]) + else: + # Unix platform options + platformOptions = [ + # use the headless QPA platform + "-qpa", "phantom", + # explicitly compile with SSL support, so build will fail if headers are missing + "-openssl", "-openssl-linked", + # disable unnecessary Qt features + "-no-openvg", + "-no-eglfs", + "-no-egl", + "-no-glib", + "-no-gtkstyle", + "-no-cups", + "-no-sm", + "-no-xinerama", + "-no-xkb", + "-no-xcb", + "-no-kms", + "-no-linuxfb", + "-no-directfb", + "-no-mtdev", + "-no-libudev", + "-no-evdev", + "-no-pulseaudio", + "-no-alsa", + "-no-feature-PRINTPREVIEWWIDGET" + ] + + if self.options.silent: + platformOptions.append("-silent") + + if platform.system() == "Darwin": + # Mac OS specific options + # NOTE: fontconfig is not required on Darwin (we use Core Text for font enumeration) + platformOptions.extend([ + "-no-pkg-config", + "-no-c++11", # Build fails on mac right now with C++11 TODO: is this still valid? + ]) + # Dirty hack to find OpenSSL libs + openssl = os.getenv("OPENSSL", "") + if openssl == "": + # search for OpenSSL + openssl_found = False + for search_path in openssl_search_paths: + if os.path.exists(search_path["header"]): + openssl_found = True + platformOptions.extend(search_path["flags"]) + print("Found OpenSSL installed via %s" % search_path["name"]) + + if not openssl_found: + raise RuntimeError("Could not find OpenSSL") + else: + # TODO: Implement + raise RuntimeError("Not implemented") + else: + # options specific to other Unixes, like Linux, BSD, ... + platformOptions.extend([ + "-fontconfig", # Fontconfig for better font matching + "-icu", # ICU for QtWebKit (which provides the OSX headers) but not QtBase + ]) + return platformOptions + + # configure Qt Base + def configureQtBase(self): + print("configuring Qt Base, please wait...") + + configureExe = os.path.abspath("src/qt/qtbase/configure") + if platform.system() == "Windows": + configureExe += ".bat" + + configure = [configureExe, + "-static", + "-opensource", + "-confirm-license", + # we use an in-source build for now and never want to install + "-prefix", os.path.abspath("src/qt/qtbase"), + # use the bundled libraries, vs. system-installed ones + "-qt-zlib", + "-qt-libpng", + "-qt-libjpeg", + "-qt-pcre", + # disable unnecessary Qt features + "-nomake", "examples", + "-nomake", "tools", + "-nomake", "tests", + "-no-qml-debug", + "-no-dbus", + "-no-opengl", + "-no-audio-backend", + "-D", "QT_NO_GRAPHICSVIEW", + "-D", "QT_NO_GRAPHICSEFFECT", + "-D", "QT_NO_STYLESHEET", + "-D", "QT_NO_STYLE_CDE", + "-D", "QT_NO_STYLE_CLEANLOOKS", + "-D", "QT_NO_STYLE_MOTIF", + "-D", "QT_NO_STYLE_PLASTIQUE", + "-D", "QT_NO_PRINTPREVIEWDIALOG" + ] + configure.extend(self.platformQtConfigureOptions()) + if self.options.qt_config: + configure.extend(self.options.qt_config) + + if self.options.debug: + configure.append("-debug") + elif self.options.release: + configure.append("-release") + else: + # build Release by default + configure.append("-release") + + if self.execute(configure, "src/qt/qtbase") != 0: + raise RuntimeError("Configuration of Qt Base failed.") + + # build Qt Base + def buildQtBase(self): + if self.options.skip_qtbase: + print("Skipping build of Qt Base") + return + + if self.options.git_clean_qtbase: + self.gitClean("src/qt/qtbase") + + if self.options.git_clean_qtbase or not self.options.skip_configure_qtbase: + self.configureQtBase() + + print("building Qt Base, please wait...") + if self.make("src/qt/qtbase") != 0: + raise RuntimeError("Building Qt Base failed.") + + # build Qt WebKit + def buildQtWebKit(self): + if self.options.skip_qtwebkit: + print("Skipping build of Qt WebKit") + return + + if self.options.git_clean_qtwebkit: + self.gitClean("src/qt/qtwebkit") + + os.putenv("SQLITE3SRCDIR", os.path.abspath("src/qt/qtbase/src/3rdparty/sqlite")) + + print("configuring Qt WebKit, please wait...") + configureOptions = [ + # disable some webkit features we do not need + "WEBKIT_CONFIG-=build_webkit2", + "WEBKIT_CONFIG-=netscape_plugin_api", + "WEBKIT_CONFIG-=use_gstreamer", + "WEBKIT_CONFIG-=use_gstreamer010", + "WEBKIT_CONFIG-=use_native_fullscreen_video", + "WEBKIT_CONFIG-=video", + "WEBKIT_CONFIG-=web_audio", + ] + if self.options.webkit_qmake_args: + configureOptions.extend(self.options.webkit_qmake_args) + if self.qmake("src/qt/qtwebkit", configureOptions) != 0: + raise RuntimeError("Configuration of Qt WebKit failed.") + + print("building Qt WebKit, please wait...") + if self.make("src/qt/qtwebkit") != 0: + raise RuntimeError("Building Qt WebKit failed.") + + # build PhantomJS + def buildPhantomJS(self): + print("Configuring PhantomJS, please wait...") + if self.qmake(".", self.options.phantomjs_qmake_args) != 0: + raise RuntimeError("Configuration of PhantomJS failed.") + print("Building PhantomJS, please wait...") + if self.make(".") != 0: + raise RuntimeError("Building PhantomJS failed.") + + # ensure the git submodules are all available + def ensureSubmodulesAvailable(self): + if self.options.skip_git: return + if self.execute(["git", "submodule", "init"], ".") != 0: + raise RuntimeError("Initialization of git submodules failed.") + if self.execute(["git", "submodule", "update", "--init"], ".") != 0: + raise RuntimeError("Initial update of git submodules failed.") + + # run all build steps required to get a final PhantomJS binary at the end + def run(self): + self.ensureSubmodulesAvailable(); + self.buildQtBase() + self.buildQtWebKit() + self.buildPhantomJS() + +# parse command line arguments and return the result +def parseArguments(): + parser = argparse.ArgumentParser(description="Build PhantomJS from sources.") + parser.add_argument("-r", "--release", action="store_true", + help="Enable compiler optimizations.") + parser.add_argument("-d", "--debug", action="store_true", + help="Build with debug symbols enabled.") + parser.add_argument("-j", "--jobs", type=int, + help="How many parallel compile jobs to use. Defaults to %d." % multiprocessing.cpu_count()) + parser.add_argument("-c", "--confirm", action="store_true", + help="Silently confirm the build.") + parser.add_argument("-n", "--dry-run", action="store_true", + help="Only print what would be done without actually executing anything.") + + # NOTE: silent build does not exist on windows apparently + if platform.system() != "Windows": + parser.add_argument("-s", "--silent", action="store_true", + help="Reduce output during compilation.") + + advanced = parser.add_argument_group("advanced options") + advanced.add_argument("--qmake-args", type=str, action="append", + help="Additional arguments that will be passed to all QMake calls.") + advanced.add_argument("--webkit-qmake-args", type=str, action="append", + help="Additional arguments that will be passed to the Qt WebKit QMake call.") + advanced.add_argument("--phantomjs-qmake-args", type=str, action="append", + help="Additional arguments that will be passed to the PhantomJS QMake call.") + advanced.add_argument("--qt-config", type=str, action="append", + help="Additional arguments that will be passed to Qt Base configure.") + advanced.add_argument("--git-clean-qtbase", action="store_true", + help="Run git clean in the Qt Base folder.\n" + "ATTENTION: This will remove all untracked files!") + advanced.add_argument("--git-clean-qtwebkit", action="store_true", + help="Run git clean in the Qt WebKit folder.\n" + "ATTENTION: This will remove all untracked files!") + advanced.add_argument("--skip-qtbase", action="store_true", + help="Skip Qt Base completely and do not build it.\n" + "Only enable this option when Qt Base was built " + "previously and no update is required.") + advanced.add_argument("--skip-configure-qtbase", action="store_true", + help="Skip configure step of Qt Base, only build it.\n" + "Only enable this option when the environment has " + "not changed and only an update of Qt Base is required.") + advanced.add_argument("--skip-qtwebkit", action="store_true", + help="Skip Qt WebKit completely and do not build it.\n" + "Only enable this option when Qt WebKit was built " + "previously and no update is required.") + advanced.add_argument("--skip-configure-qtwebkit", action="store_true", + help="Skip configure step of Qt WebKit, only build it.\n" + "Only enable this option when neither the environment nor Qt Base " + "has changed and only an update of Qt WebKit is required.") + advanced.add_argument("--skip-git", action="store_true", + help="Skip all actions that require Git. For use when building from " + "a tarball release.") + options = parser.parse_args() + if options.debug and options.release: + raise RuntimeError("Cannot build with both debug and release mode enabled.") + return options + +# main entry point which gets executed when this script is run +def main(): + # change working directory to the folder this script lives in + os.chdir(os.path.dirname(os.path.realpath(__file__))) + + try: + options = parseArguments() + if not options.confirm: + print("""\ +---------------------------------------- + WARNING +---------------------------------------- + +Building PhantomJS from source takes a very long time, anywhere from 30 minutes +to several hours (depending on the machine configuration). It is recommended to +use the premade binary packages on supported operating systems. + +For details, please go the the web site: http://phantomjs.org/download.html. +""") + while True: + sys.stdout.write("Do you want to continue (Y/n)? ") + sys.stdout.flush() + answer = sys.stdin.readline().strip().lower() + if answer == "n": + print("Cancelling PhantomJS build.") + return + elif answer == "y" or answer == "": + break + else: + print("Invalid answer, try again.") + + builder = PhantomJSBuilder(options) + builder.run() + except RuntimeError as error: + sys.stderr.write("\nERROR: Failed to build PhantomJS! %s\n" % error) + sys.stderr.flush() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/debian/README.Debian b/debian/README.Debian new file mode 100644 index 0000000..0180a05 --- /dev/null +++ b/debian/README.Debian @@ -0,0 +1,28 @@ +## Limitations + +Unlike original "phantomjs" binary that is statically linked with modified +QT+WebKit, Debian package is built with system libqt5webkit5. Unfortunately +the latter do not have webSecurity extensions therefore "--web-security=no" +is expected to fail. + + https://github.com/ariya/phantomjs/issues/13727#issuecomment-155609276 + +--- + +Ghostdriver is crippled due to removed source-less pre-built blobs: + + src/ghostdriver/third_party/webdriver-atoms/* + +Therefore all PDF functionality is broken. + +--- + +PhantomJS cannot run in headless mode (if there is no X server available). + +Unfortunately it can not be fixed in Debian. To achieve headless-ness +upstream statically link with customised QT + Webkit. We don't want to ship +forks of those projects. It would be great to eventually convince upstream +to use standard libraries. +Meanwhile one can use "xvfb-run" from "xvfb" package: + + xvfb-run --server-args="-screen 0 640x480x16" phantomjs diff --git a/debian/TODO.Debian b/debian/TODO.Debian new file mode 100644 index 0000000..e592887 --- /dev/null +++ b/debian/TODO.Debian @@ -0,0 +1,15 @@ +## Bug Selenium and Ghostdriver projects about source-less blobs. + + +## Fugure out remaining Build-Depends to successfully run test suite. + + Some of those deps probably should be recommended by binary package. + + In pbuilder tests fail with + + Qt: Session management error: Could not open network socket + + +## Figure out how to produce QPA (Lighthouse) enabled binary + + http://ariya.ofilabs.com/2012/03/pure-headless-phantomjs-no-x11-or-xvfb.html diff --git a/debian/bin/phantomjs b/debian/bin/phantomjs new file mode 100755 index 0000000..3d2f545 --- /dev/null +++ b/debian/bin/phantomjs @@ -0,0 +1,5 @@ +#!/bin/sh +LD_LIBRARY_PATH="/usr/lib/phantomjs:$LD_LIBRARY_PATH" +export LD_LIBRARY_PATH +exec "/usr/lib/phantomjs/phantomjs" "$@" + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..74ee79b --- /dev/null +++ b/debian/changelog @@ -0,0 +1,166 @@ +phantomjs (2.1.1+dfsg-2) unstable; urgency=medium + + * New patch to allow comilation with QT versions other than 5.5 + (Closes: #827421). + * Run tests under "dbus-launch"; Build-Depends += "dbus-x11". + * Respect DEB_BUILD_OPTIONS="nocheck". + * README: added note about lack of headless functionality. + * Corrected short package description (Closes: #826541). + Thanks, Mario Lang. + * Standards-Version: 3.9.8. + * Corrected Vcs-Git URL. + + -- Dmitry Smirnov Thu, 16 Jun 2016 17:26:25 +1000 + +phantomjs (2.1.1+dfsg-1) unstable; urgency=medium + + * New upstream release [January 2015]. + * Split refreshed "port-to-qt55.patch" into three patches: + + build-qt55-evaluateJavaScript.patch + + build-qt55-no-websecurity.patch + + build-qt55-print.patch + * Disabled Ghostdriver due to pre-built source-less Selenium blobs. + * Added README.Debian explaining differences from upstream "phantomjs". + * Copyright review and DFSG clean-up/repackaging. + * Run upstream test suite. + * Updated watch file. + * Added man page. + * New maintainer. + * Build-Depends: + + python + + xvfb, xauth, libgl1-mesa-dri + + gtk-vector-screenshot + + libatk-adaptor + * New patches: + + build-hardening.patch + + build-no-ghostdriver.patch + + build-qt-components.patch + + build-qtpath.patch + * New lintian-overrides. + * Removed obsolete patches: + - dont-fail-on-unrecognised-makeflags.patch + + [ Mattia Rizzolo ] + * debian/control: fix Vcs-Browser url + + -- Dmitry Smirnov Thu, 18 Feb 2016 03:25:42 +1100 + +phantomjs (2.0.0+dfsg-1) unstable; urgency=medium + + [ TANIGUCHI Takaki ] + * debian/watch: Moved to bitbucket. + * Imported Upstream version 2.0.0 (Closes: #803334, #732376, #757699) + + Fix "PDF content is scaled by 1.5277777777777777" in upstrem. + (Closes: #726254) + + Fix "SVG is not rendered in pdf" in upstream. (Closes: #726257) + + Upstream drop support CoffeeScript. (Closes: #730087) + + [ Ximin Luo ] + * Don't distribute a full obsolete copy of Qt. + * Port to Qt 5.5. + + [ TANIGUCHI Takaki ] + * Use system Qt5 library. (Closes: #784506, #679759, #699435) + * Fix "ftbfs with GCC-5" (Closes: #778061) + * debian/watch: Move to bitbucket. (Closes: #754654) + * debian/control: + + Bump-Standards Verision to 3.9.6. + + Update VCS-*. + * debian/copyright: Update source URL. + + -- TANIGUCHI Takaki Wed, 25 Nov 2015 17:41:05 +0900 + +phantomjs (1.9.2-1) unstable; urgency=low + + * Imported Upstream version 1.9.2 + * debian/control: Bump Standards-Version to 3.9.4 (with no changes). + * debian/control: Update B-D debhelper version to 9. + + -- TANIGUCHI Takaki Wed, 09 Oct 2013 17:04:18 +0900 + +phantomjs (1.9.0-1) unstable; urgency=low + + * Imported Upstream version 1.9.0 (Closes: #685404, #702381) + + Fix ""Hello world" console.log() example prints nothing" (Closes: #696212) + * Fix "FTBFS: /usr/bin/ld: cannot find -ljpeg" (Closes: #679760) + * debian/patches/0001-build-with-libjs-coffeesciprt.patch: refresh patche + * debian/install: delete src/qt/* + * debian/patches/0002-Don-t-use-ld.gold-when-building-webkit.patch: + Fix FTBFS on i386. (Closes: #693761) + + -- TANIGUCHI Takaki Wed, 15 May 2013 10:40:40 +0900 + +phantomjs (1.6.0-5) unstable; urgency=low + + * debian/install: install all customized Qt ralted libraries. + (Closes: #681505, #681829) + + -- TANIGUCHI Takaki Wed, 18 Jul 2012 15:42:26 +0900 + +phantomjs (1.6.0-4) unstable; urgency=low + + * debian/patches/return_build_error: Removed. + * debian/bin/phantomjs: Exec with args. (Closes: #681096) + + -- TANIGUCHI Takaki Wed, 11 Jul 2012 11:04:04 +0900 + +phantomjs (1.6.0-3) unstable; urgency=low + + * debian/patches/return_build_error: To fail after build error immediately. + * debian/control: libx11-dev and libxext-dev to Build-Depends. + + -- TANIGUCHI Takaki Thu, 05 Jul 2012 09:16:30 +0900 + +phantomjs (1.6.0-2) unstable; urgency=low + + * Bump Standards-Version to 3.9.3. + * debian/rules: add hardening options. + * debian/control: Add some devel libraries to B-D. (Closes: #679760) + + -- TANIGUCHI Takaki Tue, 03 Jul 2012 21:32:47 +0900 + +phantomjs (1.6.0-1) unstable; urgency=low + + * New upstream (Closes: #666397) + * Drop python-pyphantomjs. It has been removed from upstream. + + -- TANIGUCHI Takaki Sun, 01 Jul 2012 02:50:03 +0900 + +phantomjs (1.4.1+dfsg-1) unstable; urgency=low + + * Imported Upstream version 1.4.1 + + -- TANIGUCHI Takaki Fri, 10 Feb 2012 10:24:27 +0900 + +phantomjs (1.4.0+dfsg-1) unstable; urgency=low + + * New upstream + * debian/patches/0001-build-with-libjs-coffeesciprt.patch: Follow + upstream changes. + + -- TANIGUCHI Takaki Tue, 27 Dec 2011 22:56:20 +0900 + +phantomjs (1.3.0+dfsg-4) unstable; urgency=low + + * debian/patches/0001-build-with-libjs-coffeesciprt.patch: Fixed + coffee-script.js path. (Closes: #651171) + + -- TANIGUCHI Takaki Tue, 06 Dec 2011 22:37:13 +0900 + +phantomjs (1.3.0+dfsg-3) unstable; urgency=low + + * debian/control: Add python-qt4 to Depends (Closes: #648409) + + -- TANIGUCHI Takaki Tue, 15 Nov 2011 11:31:06 +0900 + +phantomjs (1.3.0+dfsg-2) unstable; urgency=low + + * debian/watch: improve pattern to return correct value. + + -- TANIGUCHI Takaki Tue, 01 Nov 2011 09:16:37 +0900 + +phantomjs (1.3.0+dfsg-1) unstable; urgency=low + + * Initial release (Closes: #646556) + + -- TANIGUCHI Takaki Tue, 25 Oct 2011 15:13:08 +0900 diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..6ee052c --- /dev/null +++ b/debian/clean @@ -0,0 +1,66 @@ +test/www/*.pyc +src/*.o +src/moc_*.cpp +bin/phantomjs + +src/qt/lib/fonts +src/qt/mkspecs/default +src/qt/bin/qmake +src/qt/config.tests/unix/3dnow/3dnow.o +src/qt/config.tests/unix/3dnow/3dnow +src/qt/config.tests/unix/alsa/alsatest.o +src/qt/config.tests/unix/alsa/alsa +src/qt/config.tests/unix/avx/avx.o +src/qt/config.tests/unix/avx/avx +src/qt/config.tests/unix/clock-gettime/clock-gettime.o +src/qt/config.tests/unix/clock-gettime/clock-gettime +src/qt/config.tests/unix/clock-monotonic/clock-monotonic.o +src/qt/config.tests/unix/clock-monotonic/clock-monotonic +src/qt/config.tests/unix/floatmath/floatmath.o +src/qt/config.tests/unix/floatmath/floatmath +src/qt/config.tests/unix/getaddrinfo/getaddrinfotest.o +src/qt/config.tests/unix/getaddrinfo/getaddrinfo +src/qt/config.tests/unix/getifaddrs/getifaddrs.o +src/qt/config.tests/unix/getifaddrs/getifaddrs +src/qt/config.tests/unix/inotify/inotifytest.o +src/qt/config.tests/unix/inotify/inotify +src/qt/config.tests/unix/ipc_sysv/ipc.o +src/qt/config.tests/unix/ipc_sysv/ipc_sysv +src/qt/config.tests/unix/ipv6/ipv6test.o +src/qt/config.tests/unix/ipv6/ipv6 +src/qt/config.tests/unix/ipv6ifname/ipv6ifname.o +src/qt/config.tests/unix/ipv6ifname/ipv6ifname +src/qt/config.tests/unix/mmx/mmx.o +src/qt/config.tests/unix/mmx/mmx +src/qt/config.tests/unix/mremap/mremap.o +src/qt/config.tests/unix/mremap/mremap +src/qt/config.tests/unix/nis/nis.o +src/qt/config.tests/unix/nis/nis +src/qt/config.tests/unix/openssl/openssl.o +src/qt/config.tests/unix/openssl/openssl +src/qt/config.tests/unix/ptrsize/ptrsizetest.o +src/qt/config.tests/unix/ptrsize/ptrsizetest +src/qt/config.tests/unix/sse/sse.o +src/qt/config.tests/unix/sse/sse +src/qt/config.tests/unix/sse2/sse2.o +src/qt/config.tests/unix/sse2/sse2 +src/qt/config.tests/unix/sse3/sse3.o +src/qt/config.tests/unix/sse3/sse3 +src/qt/config.tests/unix/sse4_1/sse4_1.o +src/qt/config.tests/unix/sse4_1/sse4_1 +src/qt/config.tests/unix/sse4_2/sse4_2.o +src/qt/config.tests/unix/sse4_2/sse4_2 +src/qt/config.tests/unix/ssse3/ssse3.o +src/qt/config.tests/unix/ssse3/ssse3 +src/qt/config.tests/unix/stdint/main.o +src/qt/config.tests/unix/stdint/stdint +src/qt/config.tests/unix/stl/stltest.o +src/qt/config.tests/unix/stl/stl +src/qt/config.tests/x11/fontconfig/fontconfig.o +src/qt/config.tests/x11/fontconfig/fontconfig +src/qt/lib/libjscore.a +src/qt/lib/libwebcore.a +src/qt/src/3rdparty/webkit/Source/JavaScriptCore/release/libjscore.a +src/qt/src/3rdparty/webkit/Source/WebCore/release/libwebcore.a +src/qt/.qmake.vars + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..688b368 --- /dev/null +++ b/debian/control @@ -0,0 +1,43 @@ +Source: phantomjs +Section: web +Priority: extra +Maintainer: Dmitry Smirnov +Uploaders: TANIGUCHI Takaki +Build-Depends: debhelper (>= 9), + libjs-coffeescript, + libfreetype6-dev, + libjpeg-dev, + libpng-dev, + libsqlite3-dev, + libssl-dev, + libz-dev, + libfontconfig1-dev, + libx11-dev, + libxext-dev, + libqt5webkit5-dev, + python, + qt5-qmake, + qtchooser +# Generic tests: + ,dbus-x11 + ,gtk-vector-screenshot + ,libatk-adaptor + ,xvfb, xauth, libgl1-mesa-dri +# Ghostdriver tests: +# ,libgradle-core-java +Standards-Version: 3.9.8 +Homepage: http://www.phantomjs.org/ +Vcs-Git: https://anonscm.debian.org/git/collab-maint/phantomjs.git +Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/phantomjs.git + +Package: phantomjs +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: minimalistic headless WebKit-based browser with JavaScript API + PhantomJS is a headless WebKit with JavaScript API. It has fast and native + support for various web standards: DOM handling, CSS selector, JSON, Canvas, + and SVG. + . + PhantomJS is an optimal solution for headless testing of web-based + applications, site scraping, pages capture, SVG renderer, PDF converter + and many other use cases. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..67ad349 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,197 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: phantomjs +Source: https://github.com/ariya/phantomjs +Files-Excluded: + src/qt/* + src/ghostdriver/third_party/webdriver-atoms/* + test/ghostdriver-test/java/gradle/gradle-wrapper.jar + test/ghostdriver-test/fixtures/common/css/ui-lightness/jquery-ui-1.8.10.custom.css + test/ghostdriver-test/fixtures/common/js/jquery-1.4.4.min.js + test/ghostdriver-test/fixtures/common/js/jquery-ui-1.8.10.custom.min.js + test/www/regression/pjs-10690/Windsong.ttf + +Files: * +Copyright: + 2011-2012 Ariya Hidayat + 2011-2012 Ivan De Marino + 2012-2013 James M. Greene + 2011 James Roe + 2013 Joseph Rollinson + 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + 2012,2014 Milian Wolff, KDAB + 2015 Zachary Weinberg + 2011-2012 execjosh, http://execjosh.blogspot.com +License: BSD-3-Clause + +Files: examples/modernizr.js +Copyright: 2009-2011 Faruk Ates, Paul Irish, Alex Sexton +License: BSD-3-Clause or Expat + +Files: deploy/brandelf.c +Copyright: + 2000,2001 David O'Brien + 1996 Søren Schmidt +License: BSD-3-Clause + +Files: + src/ghostdriver/* + test/ghostdriver-test/java/src/test/java/ghostdriver/* +Copyright: + 2012-2014 Ivan De Marino + 2014 Alex Anderson <@alxndrsn> + 2014 Jim Evans - Salesforce.com + 2014 Steven Levithan +License: BSD-2-Clause + +Files: + test/ghostdriver-test/fixtures/common/form_handling_js_submit.html +Copyright: + 2012 Selenium committers + 2012 Software Freedom Conservancy +License: Apache-2.0 + +Files: + test/ghostdriver-test/fixtures/common/jquery-1.3.2.js + test/ghostdriver-test/fixtures/common/css/ui-lightness/* +Copyright: + 2010-2011 AUTHORS.txt (http://jqueryui.com/about) + 2009-2010 John Resig + 2009-2010 the Dojo Foundation +License: Expat + +Files: + src/ghostdriver/third_party/uuid.js +Copyright: + 2010 Robert Kieffer +License: Expat +Comment: https://github.com/broofa/node-uuid/blob/master/LICENSE.md + +Files: + src/linenoise/* +Copyright: + 2010-2014 Salvatore Sanfilippo + 2010-2013 Pieter Noordhuis + 2010-2011 Steve Bennett + 2011 Alan DeKok +License: BSD-2-Clause +Comment: https://github.com/antirez/linenoise/blob/master/LICENSE + +Files: + src/mongoose/* +Copyright: + 2004-2010 Sergey Lyubka +License: Expat + +Files: + src/qcommandline/* +Copyright: + 2010-2011 Corentin Chary +License: LGPL-2+ + +Files: + test/www/regression/pjs-10690/jquery.js +Copyright: + 2012 jQuery Foundation and other contributors +License: Expat + +Files: debian/* +Copyright: + 2011-2015 TANIGUCHI Takaki + 2016 Dmitry Smirnov +License: BSD-3-Clause + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + ․ + http://www.apache.org/licenses/LICENSE-2.0 + ․ + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ․ + On Debian systems, the complete text of the Apache License, + Version 2.0 can be found in "/usr/share/common-licenses/Apache-2.0". + +License: BSD-2-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + . + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: BSD-3-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: Expat + 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. + +License: LGPL-2+ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + ․ + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + ․ + On Debian systems, the complete text of the GNU Library General Public + License Version 2 can be found in "/usr/share/common-licenses/LGPL-2". + diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/debian/examples b/debian/examples new file mode 100644 index 0000000..e39721e --- /dev/null +++ b/debian/examples @@ -0,0 +1 @@ +examples/* diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..01fc847 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,7 @@ + +[dch] +id-length = 0 + +[import-orig] +pristine-tar = True +merge = True diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..cc4b345 --- /dev/null +++ b/debian/install @@ -0,0 +1,3 @@ +debian/bin/phantomjs usr/bin +bin/phantomjs usr/lib/phantomjs +# src/qt/lib/lib*so* usr/lib/phantomjs diff --git a/debian/manpages b/debian/manpages new file mode 100644 index 0000000..0f65186 --- /dev/null +++ b/debian/manpages @@ -0,0 +1 @@ +debian/*.1 diff --git a/debian/patches/build-hardening.patch b/debian/patches/build-hardening.patch new file mode 100644 index 0000000..81bbdde --- /dev/null +++ b/debian/patches/build-hardening.patch @@ -0,0 +1,16 @@ +Last-Update: 2016-02-15 +Forwarded: not-needed +Author: Dmitry Smirnov +Description: QMAKE hardening. + +--- a/src/phantomjs.pro ++++ b/src/phantomjs.pro +@@ -1,4 +1,8 @@ ++## https://wiki.debian.org/Hardening#Notes_for_packages_using_QMake ++QMAKE_CFLAGS *= $$(CFLAGS) $$(CPPFLAGS) ++QMAKE_CXXFLAGS *= $$(CXXFLAGS) $$(CPPFLAGS) ++QMAKE_LFLAGS *= $$(LDFLAGS) + + if(!equals(QT_MAJOR_VERSION, 5)|!equals(QT_MINOR_VERSION, 5)) { + error("This program can only be compiled with Qt 5.5.x.") + } diff --git a/debian/patches/build-no-ghostdriver.patch b/debian/patches/build-no-ghostdriver.patch new file mode 100644 index 0000000..b0462f9 --- /dev/null +++ b/debian/patches/build-no-ghostdriver.patch @@ -0,0 +1,63 @@ +Last-Update: 2016-02-15 +Forwarded: not-needed +Author: Dmitry Smirnov +Description: fix compilation without non-DFSG Selenium "ghostdriver/third_party/webdriver-atoms/*" files + +--- a/src/ghostdriver/ghostdriver.qrc ++++ b/src/ghostdriver/ghostdriver.qrc +@@ -17,55 +17,8 @@ + third_party/console++.js + third_party/har.js + third_party/parseuri.js + third_party/uuid.js +- third_party/webdriver-atoms/active_element.js +- third_party/webdriver-atoms/clear.js +- third_party/webdriver-atoms/clear_local_storage.js +- third_party/webdriver-atoms/clear_session_storage.js +- third_party/webdriver-atoms/click.js +- third_party/webdriver-atoms/double_click.js +- third_party/webdriver-atoms/drag.js +- third_party/webdriver-atoms/execute_async_script.js +- third_party/webdriver-atoms/execute_script.js +- third_party/webdriver-atoms/execute_sql.js +- third_party/webdriver-atoms/find_element.js +- third_party/webdriver-atoms/find_elements.js +- third_party/webdriver-atoms/focus_on_element.js +- third_party/webdriver-atoms/get_appcache_status.js +- third_party/webdriver-atoms/get_attribute.js +- third_party/webdriver-atoms/get_attribute_value.js +- third_party/webdriver-atoms/get_current_position.js +- third_party/webdriver-atoms/get_local_storage_item.js +- third_party/webdriver-atoms/get_local_storage_keys.js +- third_party/webdriver-atoms/get_local_storage_size.js +- third_party/webdriver-atoms/get_location.js +- third_party/webdriver-atoms/get_location_in_view.js +- third_party/webdriver-atoms/get_session_storage_item.js +- third_party/webdriver-atoms/get_session_storage_keys.js +- third_party/webdriver-atoms/get_session_storage_size.js +- third_party/webdriver-atoms/get_size.js +- third_party/webdriver-atoms/get_text.js +- third_party/webdriver-atoms/get_value_of_css_property.js +- third_party/webdriver-atoms/is_displayed.js +- third_party/webdriver-atoms/is_enabled.js +- third_party/webdriver-atoms/is_online.js +- third_party/webdriver-atoms/is_selected.js +- third_party/webdriver-atoms/lastupdate +- third_party/webdriver-atoms/move_mouse.js +- third_party/webdriver-atoms/pinch.js +- third_party/webdriver-atoms/remove_local_storage_item.js +- third_party/webdriver-atoms/remove_session_storage_item.js +- third_party/webdriver-atoms/right_click.js +- third_party/webdriver-atoms/rotate.js +- third_party/webdriver-atoms/scroll_into_view.js +- third_party/webdriver-atoms/scroll_mouse.js +- third_party/webdriver-atoms/set_local_storage_item.js +- third_party/webdriver-atoms/set_session_storage_item.js +- third_party/webdriver-atoms/submit.js +- third_party/webdriver-atoms/swipe.js +- third_party/webdriver-atoms/tap.js +- third_party/webdriver-atoms/type.js + webdriver_atoms.js + webelementlocator.js + + diff --git a/debian/patches/build-qt-components.patch b/debian/patches/build-qt-components.patch new file mode 100644 index 0000000..de52fb4 --- /dev/null +++ b/debian/patches/build-qt-components.patch @@ -0,0 +1,18 @@ +Last-Update: 2016-02-15 +Forwarded: no +Author: Dmitry Smirnov +Description: fix FTBFS. + +--- a/src/phantomjs.pro ++++ b/src/phantomjs.pro +@@ -8,9 +8,9 @@ + } + + TEMPLATE = app + TARGET = phantomjs +-QT += network webkitwidgets ++QT += network webkitwidgets printsupport + CONFIG += console + + DESTDIR = ../bin + diff --git a/debian/patches/build-qt55-evaluateJavaScript.patch b/debian/patches/build-qt55-evaluateJavaScript.patch new file mode 100644 index 0000000..ba1f530 --- /dev/null +++ b/debian/patches/build-qt55-evaluateJavaScript.patch @@ -0,0 +1,169 @@ +Last-Update: 2016-02-15 +Forwarded: no +Bug-Upstream: https://github.com/ariya/phantomjs/issues/13727 +Author: Ximin Luo +Reviewed-By: Dmitry Smirnov +Description: Port to Qt 5.5 + - Remove second argument to evaluateJavascript(), which was not really used + for anything, anyways + +--- a/src/utils.cpp ++++ b/src/utils.cpp +@@ -37,8 +37,9 @@ + #include + #include + #include + ++ + static QString findScript(const QString& jsFilePath, const QString& libraryPath) + { + if (!jsFilePath.isEmpty()) { + QFile jsFile; +@@ -131,9 +132,9 @@ + } + return false; + } + // Execute JS code in the context of the document +- targetFrame->evaluateJavaScript(scriptBody, QString(JAVASCRIPT_SOURCE_CODE_URL).arg(QFileInfo(scriptPath).fileName())); ++ targetFrame->evaluateJavaScript(scriptBody); + return true; + } + + bool loadJSForDebug(const QString& jsFilePath, const QString& libraryPath, QWebFrame* targetFrame, const bool autorun) +@@ -146,12 +147,12 @@ + QString scriptPath = findScript(jsFilePath, libraryPath); + QString scriptBody = jsFromScriptFile(scriptPath, jsFileLanguage, jsFileEnc); + + scriptBody = QString("function __run() {\n%1\n}").arg(scriptBody); +- targetFrame->evaluateJavaScript(scriptBody, QString(JAVASCRIPT_SOURCE_CODE_URL).arg(QFileInfo(scriptPath).fileName())); ++ targetFrame->evaluateJavaScript(scriptBody); + + if (autorun) { +- targetFrame->evaluateJavaScript("__run()", QString()); ++ targetFrame->evaluateJavaScript("__run()"); + } + + return true; + } +--- a/src/repl.cpp ++++ b/src/repl.cpp +@@ -146,9 +146,9 @@ + // Set the static callback to offer Completions to the User + linenoiseSetCompletionCallback(REPL::offerCompletion); + + // Inject REPL utility functions +- m_webframe->evaluateJavaScript(Utils::readResourceFileUtf8(":/repl.js"), QString(JAVASCRIPT_SOURCE_PLATFORM_URL).arg("repl.js")); ++ m_webframe->evaluateJavaScript(Utils::readResourceFileUtf8(":/repl.js")); + + // Add self to JavaScript world + m_webframe->addToJavaScriptWindowObject("_repl", this); + +@@ -183,10 +183,9 @@ + // This will return an array of String with the possible completions + QStringList completions = REPL::getInstance()->m_webframe->evaluateJavaScript( + QString(JS_RETURN_POSSIBLE_COMPLETIONS).arg( + toInspect, +- toComplete), +- QString() ++ toComplete) + ).toStringList(); + + foreach(QString c, completions) { + if (lastIndexOfDot > -1) { +@@ -209,9 +208,9 @@ + if (userInput[0] != '\0') { + // Send the user input to the main Phantom frame for evaluation + m_webframe->evaluateJavaScript( + QString(JS_EVAL_USER_INPUT).arg( +- QString(userInput).replace('"', "\\\"")), QString("phantomjs://repl-input")); ++ QString(userInput).replace('"', "\\\""))); + + // Save command in the REPL history + linenoiseHistoryAdd(userInput); + linenoiseHistorySave(m_historyFilepath.data()); //< requires "char *" +--- a/src/phantom.cpp ++++ b/src/phantom.cpp +@@ -380,9 +380,9 @@ + "require.cache['" + filename + "']._getRequire()," + + "require.cache['" + filename + "'].exports," + + "require.cache['" + filename + "']" + + "));"; +- m_page->mainFrame()->evaluateJavaScript(scriptSource, QString(JAVASCRIPT_SOURCE_PLATFORM_URL).arg(QFileInfo(filename).fileName())); ++ m_page->mainFrame()->evaluateJavaScript(scriptSource); + } + + bool Phantom::injectJs(const QString& jsFilePath) + { +@@ -477,10 +477,9 @@ + m_page->mainFrame()->addToJavaScriptWindowObject("phantom", this); + + // Bootstrap the PhantomJS scope + m_page->mainFrame()->evaluateJavaScript( +- Utils::readResourceFileUtf8(":/bootstrap.js"), +- QString(JAVASCRIPT_SOURCE_PLATFORM_URL).arg("bootstrap.js") ++ Utils::readResourceFileUtf8(":/bootstrap.js") + ); + } + + bool Phantom::setCookies(const QVariantList& cookies) +--- a/src/config.cpp ++++ b/src/config.cpp +@@ -176,9 +176,9 @@ + QWebPage webPage; + // Add this object to the global scope + webPage.mainFrame()->addToJavaScriptWindowObject("config", this); + // Apply the JSON config settings to this very object +- webPage.mainFrame()->evaluateJavaScript(configurator.arg(jsonConfig), QString()); ++ webPage.mainFrame()->evaluateJavaScript(configurator.arg(jsonConfig)); + } + + QString Config::helpText() const + { +--- a/src/webpage.cpp ++++ b/src/webpage.cpp +@@ -752,10 +752,10 @@ + + qDebug() << "WebPage - evaluateJavaScript" << function; + + evalResult = m_currentFrame->evaluateJavaScript( +- function, //< function evaluated +- QString("phantomjs://webpage.evaluate()")); //< reference source file ++ function //< function evaluated ++ ); + + qDebug() << "WebPage - evaluateJavaScript result" << evalResult; + + return evalResult; +@@ -925,9 +925,9 @@ + networkOp = QNetworkAccessManager::DeleteOperation; + } + + if (networkOp == QNetworkAccessManager::UnknownOperation) { +- m_mainFrame->evaluateJavaScript("console.error('Unknown network operation: " + operation + "');", QString()); ++ m_mainFrame->evaluateJavaScript("console.error('Unknown network operation: " + operation + "');"); + return; + } + + if (address == "about:blank") { +@@ -1314,9 +1314,9 @@ + return ret.toString(); + } + } + } +- frame->evaluateJavaScript("console.error('Bad header callback given, use phantom.callback);", QString()); ++ frame->evaluateJavaScript("console.error('Bad header callback given, use phantom.callback);"); + return QString(); + } + + QString WebPage::header(int page, int numPages) +@@ -1353,9 +1353,9 @@ + } + + void WebPage::_appendScriptElement(const QString& scriptUrl) + { +- m_currentFrame->evaluateJavaScript(QString(JS_APPEND_SCRIPT_ELEMENT).arg(scriptUrl), scriptUrl); ++ m_currentFrame->evaluateJavaScript(QString(JS_APPEND_SCRIPT_ELEMENT).arg(scriptUrl)); + } + + QObject* WebPage::_getGenericCallback() + { diff --git a/debian/patches/build-qt55-no-websecurity.patch b/debian/patches/build-qt55-no-websecurity.patch new file mode 100644 index 0000000..9311c93 --- /dev/null +++ b/debian/patches/build-qt55-no-websecurity.patch @@ -0,0 +1,54 @@ +Last-Update: 2016-02-15 +Forwarded: not-needed +Bug-Upstream: https://github.com/ariya/phantomjs/issues/13727 +Author: Ximin Luo +Reviewed-By: Dmitry Smirnov +Description: Port to Qt 5.5 + - Remove "webSecurity" option since that's not exposed in Qt 5.5 + +--- a/src/consts.h ++++ b/src/consts.h +@@ -63,9 +63,8 @@ + #define PAGE_SETTINGS_USERNAME "userName" + #define PAGE_SETTINGS_PASSWORD "password" + #define PAGE_SETTINGS_MAX_AUTH_ATTEMPTS "maxAuthAttempts" + #define PAGE_SETTINGS_RESOURCE_TIMEOUT "resourceTimeout" +-#define PAGE_SETTINGS_WEB_SECURITY_ENABLED "webSecurityEnabled" + #define PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS "javascriptCanOpenWindows" + #define PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS "javascriptCanCloseWindows" + + #define DEFAULT_WEBDRIVER_CONFIG "127.0.0.1:8910" +--- a/src/phantom.cpp ++++ b/src/phantom.cpp +@@ -135,9 +135,8 @@ + m_defaultPageSettings[PAGE_SETTINGS_JS_ENABLED] = QVariant::fromValue(true); + m_defaultPageSettings[PAGE_SETTINGS_XSS_AUDITING] = QVariant::fromValue(false); + m_defaultPageSettings[PAGE_SETTINGS_USER_AGENT] = QVariant::fromValue(m_page->userAgent()); + m_defaultPageSettings[PAGE_SETTINGS_LOCAL_ACCESS_REMOTE] = QVariant::fromValue(m_config.localToRemoteUrlAccessEnabled()); +- m_defaultPageSettings[PAGE_SETTINGS_WEB_SECURITY_ENABLED] = QVariant::fromValue(m_config.webSecurityEnabled()); + m_defaultPageSettings[PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS] = QVariant::fromValue(m_config.javascriptCanOpenWindows()); + m_defaultPageSettings[PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS] = QVariant::fromValue(m_config.javascriptCanCloseWindows()); + m_page->applySettings(m_defaultPageSettings); + +--- a/src/webpage.cpp ++++ b/src/webpage.cpp +@@ -367,9 +367,8 @@ + // attribute "WebSecurityEnabled" must be applied during the initializing + // security context for Document instance. Setting up it later will not cause any effect + // see + QWebSettings* settings = m_customWebPage->settings(); +- settings->setAttribute(QWebSettings::WebSecurityEnabled, phantomCfg->webSecurityEnabled()); + + m_mainFrame = m_customWebPage->mainFrame(); + m_currentFrame = m_mainFrame; + m_mainFrame->setHtml(BLANK_HTML, baseUrl); +@@ -621,9 +620,8 @@ + opt->setAttribute(QWebSettings::AutoLoadImages, def[PAGE_SETTINGS_LOAD_IMAGES].toBool()); + opt->setAttribute(QWebSettings::JavascriptEnabled, def[PAGE_SETTINGS_JS_ENABLED].toBool()); + opt->setAttribute(QWebSettings::XSSAuditingEnabled, def[PAGE_SETTINGS_XSS_AUDITING].toBool()); + opt->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, def[PAGE_SETTINGS_LOCAL_ACCESS_REMOTE].toBool()); +- opt->setAttribute(QWebSettings::WebSecurityEnabled, def[PAGE_SETTINGS_WEB_SECURITY_ENABLED].toBool()); + opt->setAttribute(QWebSettings::JavascriptCanOpenWindows, def[PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS].toBool()); + opt->setAttribute(QWebSettings::JavascriptCanCloseWindows, def[PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS].toBool()); + + if (def.contains(PAGE_SETTINGS_USER_AGENT)) { diff --git a/debian/patches/build-qt55-print.patch b/debian/patches/build-qt55-print.patch new file mode 100644 index 0000000..62984c5 --- /dev/null +++ b/debian/patches/build-qt55-print.patch @@ -0,0 +1,34 @@ +Last-Update: 2016-02-15 +Forwarded: no +Bug-Upstream: https://github.com/ariya/phantomjs/issues/13727 +Author: Ximin Luo +Reviewed-By: Dmitry Smirnov +Description: Port to Qt 5.5 + - In webpage, don't inherit QWebFrame::PrintCallback since it's not public + +--- a/src/webpage.cpp ++++ b/src/webpage.cpp +@@ -1254,9 +1254,9 @@ + } + + printer.setPageMargins(marginLeft, marginTop, marginRight, marginBottom, QPrinter::Point); + +- m_mainFrame->print(&printer, this); ++ m_mainFrame->print(&printer); + return true; + } + + void WebPage::setZoomFactor(qreal zoom) +--- a/src/webpage.h ++++ b/src/webpage.h +@@ -44,9 +44,9 @@ + class NetworkAccessManager; + class QWebInspector; + class Phantom; + +-class WebPage : public QObject, public QWebFrame::PrintCallback ++class WebPage : public QObject + { + Q_OBJECT + Q_PROPERTY(QString title READ title) + Q_PROPERTY(QString frameTitle READ frameTitle) diff --git a/debian/patches/build-qtpath.patch b/debian/patches/build-qtpath.patch new file mode 100644 index 0000000..b60b439 --- /dev/null +++ b/debian/patches/build-qtpath.patch @@ -0,0 +1,18 @@ +Last-Update: 2016-02-15 +Forwarded: not-needed +Author: Dmitry Smirnov +Description: build system correction(s). + +--- a/build.py ++++ b/build.py +@@ -80,9 +80,9 @@ + def qmakePath(): + exe = "qmake" + if platform.system() == "Windows": + exe += ".exe" +- return os.path.abspath("src/qt/qtbase/bin/" + exe) ++ return os.path.abspath("/usr/bin/" + exe) + + # returns paths for 3rd party libraries (Windows only) + def findThirdPartyDeps(): + include_dirs = [] diff --git a/debian/patches/ignore-upstream-debian-vcs-ignores.patch b/debian/patches/ignore-upstream-debian-vcs-ignores.patch new file mode 100644 index 0000000..699ddad --- /dev/null +++ b/debian/patches/ignore-upstream-debian-vcs-ignores.patch @@ -0,0 +1,21 @@ +Last-Update: 2015-11-11 +Author: Ximin Luo +Description: fix .gitignore in a better way + - rm debian-specific things from upstream .gitignore + +--- a/.gitignore ++++ b/.gitignore +@@ -10,13 +10,8 @@ + *.o + *.swp + *.pyc + *.a +-/debian/*.debhelper +-/debian/files +-/debian/*.log +-/debian/*.substvars +-/debian/*/ + /deploy/qt-*.tar.gz + /deploy/Qt-* + /symbols + /src/qt/qtc-debugging-helper diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..be3f7dc --- /dev/null +++ b/debian/patches/series @@ -0,0 +1,9 @@ +build-hardening.patch +build-no-ghostdriver.patch +build-qt-components.patch +build-qt55-evaluateJavaScript.patch +build-qt55-no-websecurity.patch +build-qt55-print.patch +build-qtpath.patch +ignore-upstream-debian-vcs-ignores.patch +unlock-qt.patch diff --git a/debian/patches/unlock-qt.patch b/debian/patches/unlock-qt.patch new file mode 100644 index 0000000..2702644 --- /dev/null +++ b/debian/patches/unlock-qt.patch @@ -0,0 +1,20 @@ +Last-Update: 2016-06-16 +Forwarded: not-needed +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=827421 +Author: Dmitry Smirnov +Description: allow build with QT-5.5+ + +--- a/src/phantomjs.pro ++++ b/src/phantomjs.pro +@@ -2,11 +2,8 @@ + QMAKE_CFLAGS *= $$(CFLAGS) $$(CPPFLAGS) + QMAKE_CXXFLAGS *= $$(CXXFLAGS) $$(CPPFLAGS) + QMAKE_LFLAGS *= $$(LDFLAGS) + +-if(!equals(QT_MAJOR_VERSION, 5)|!equals(QT_MINOR_VERSION, 5)) { +- error("This program can only be compiled with Qt 5.5.x.") +-} + + TEMPLATE = app + TARGET = phantomjs + QT += network webkitwidgets printsupport diff --git a/debian/phantomjs.1 b/debian/phantomjs.1 new file mode 100644 index 0000000..72cda4f --- /dev/null +++ b/debian/phantomjs.1 @@ -0,0 +1,125 @@ +.\" This file was pre-generated by help2man 1.47.3. +.TH PHANTOMJS "1" "February 2016" "phantomjs 2.1.1" "User Commands" +.SH NAME +phantomjs \- headless WebKit scriptable with a JavaScript API +.SH DESCRIPTION +PhantomJS is a headless WebKit scriptable with a JavaScript API. It has +fast and native support for various web standards: DOM handling, CSS +selector, JSON, Canvas, and SVG. +.SH SYNOPSIS +phantomjs [switchs] [options] [script] [argument [argument [...]]] +.SH OPTIONS +.TP +\fB\-\-cookies\-file=\fR +Sets the file name to store the persistent cookies +.TP +\fB\-\-config=\fR +Specifies JSON\-formatted configuration file +.TP +\fB\-\-debug=\fR +Prints additional warning and debug message: 'true' or 'false' (default) +.TP +\fB\-\-disk\-cache=\fR +Enables disk cache: 'true' or 'false' (default) +.TP +\fB\-\-disk\-cache\-path=\fR +Specifies the location for the disk cache +.TP +\fB\-\-ignore\-ssl\-errors=\fR +Ignores SSL errors (expired/self\-signed certificate errors): 'true' or 'false' (default) +.TP +\fB\-\-load\-images=\fR +Loads all inlined images: 'true' (default) or 'false' +.TP +\fB\-\-local\-url\-access=\fR +Allows use of 'file:///' URLs: 'true' (default) or 'false' +.TP +\fB\-\-local\-storage\-path=\fR +Specifies the location for local storage +.TP +\fB\-\-local\-storage\-quota=\fR +Sets the maximum size of the local storage (in KB) +.TP +\fB\-\-offline\-storage\-path=\fR +Specifies the location for offline storage +.TP +\fB\-\-offline\-storage\-quota=\fR +Sets the maximum size of the offline storage (in KB) +.TP +\fB\-\-local\-to\-remote\-url\-access=\fR +Allows local content to access remote URL: 'true' or 'false' (default) +.TP +\fB\-\-max\-disk\-cache\-size=\fR +Limits the size of the disk cache (in KB) +.TP +\fB\-\-output\-encoding=\fR +Sets the encoding for the terminal output, default is 'utf8' +.TP +\fB\-\-remote\-debugger\-port=\fR +Starts the script in a debug harness and listens on the specified port +.TP +\fB\-\-remote\-debugger\-autorun=\fR +Runs the script in the debugger immediately: 'true' or 'false' (default) +.TP +\fB\-\-proxy=\fR +Sets the proxy server, e.g. '\-\-proxy=http://proxy.company.com:8080' +.TP +\fB\-\-proxy\-auth=\fR +Provides authentication information for the proxy, e.g. ''\-proxy\-auth=username:password' +.TP +\fB\-\-proxy\-type=\fR +Specifies the proxy type, 'http' (default), 'none' (disable completely), or 'socks5' +.TP +\fB\-\-script\-encoding=\fR +Sets the encoding used for the starting script, default is 'utf8' +.TP +\fB\-\-script\-language=\fR +Sets the script language instead of detecting it: 'javascript' +.TP +\fB\-\-web\-security=\fR +Enables web security, 'true' (default) or 'false' +.TP +\fB\-\-ssl\-protocol=\fR +Selects a specific SSL protocol version to offer. Values (case insensitive): TLSv1.2, TLSv1.1, TLSv1.0, TLSv1 (same as v1.0), SSLv3, or ANY. Default is to offer all that Qt thinks are secure (SSLv3 and up). Not all values may be supported, depending on the system OpenSSL library. +.TP +\fB\-\-ssl\-ciphers=\fR +Sets supported TLS/SSL ciphers. Argument is a colon\-separated list of OpenSSL cipher names (macros like ALL, kRSA, etc. may not be used). Default matches modern browsers. +.TP +\fB\-\-ssl\-certificates\-path=\fR +Sets the location for custom CA certificates (if none set, uses environment variable SSL_CERT_DIR. If none set too, uses system default) +.TP +\fB\-\-ssl\-client\-certificate\-file=\fR +Sets the location of a client certificate +.TP +\fB\-\-ssl\-client\-key\-file=\fR +Sets the location of a clients' private key +.TP +\fB\-\-ssl\-client\-key\-passphrase=\fR +Sets the passphrase for the clients' private key +.TP +\fB\-\-webdriver=\fR +Starts in 'Remote WebDriver mode' (embedded GhostDriver): '[[:]]' (default '127.0.0.1:8910') +.TP +\fB\-\-webdriver\-logfile=\fR +File where to write the WebDriver's Log (default 'none') (NOTE: needs '\-\-webdriver') +.TP +\fB\-\-webdriver\-loglevel=\fR +WebDriver Logging Level: (supported: 'ERROR', 'WARN', 'INFO', 'DEBUG') (default 'INFO') (NOTE: needs '\-\-webdriver') +.TP +\fB\-\-webdriver\-selenium\-grid\-hub=\fR +URL to the Selenium Grid HUB: 'URL_TO_HUB' (default 'none') (NOTE: needs '\-\-webdriver') +.TP +\fB\-w\fR,\-\-wd +Equivalent to '\-\-webdriver' option above +.TP +\fB\-h\fR,\-\-help +Shows this message and quits +.TP +\fB\-v\fR,\-\-version +Prints out PhantomJS version +.PP +Any of the options that accept boolean values ('true'/'false') can also accept 'yes'/'no'. +.PP +Without any argument, PhantomJS will launch in interactive mode (REPL). +.PP +Documentation can be found at the web site, http://phantomjs.org. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..aa8e8b3 --- /dev/null +++ b/debian/rules @@ -0,0 +1,28 @@ +#!/usr/bin/make -f + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +export DEB_BUILD_MAINT_OPTIONS=hardening=+all + +#QT_CFG=-no-rpath -system-zlib -system-libjpeg -system-libpng \ +# -system-sqlite -plugin-sql-sqlite + +export QT_SELECT = qt5 + +%: + dh $@ + +override_dh_auto_build: + python build.py --skip-git --skip-qtbase --skip-qtwebkit --confirm --release + +override_dh_shlibdeps: + dh_shlibdeps -l/usr/lib/phantomjs + +override_dh_auto_test: +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + ## xvfb-run is affected by #814607. + ## By default Xvfb starts at 8bpp (GLX doesn't work at 8bpp). + ## https://bugzilla.redhat.com/show_bug.cgi?id=904851 + -dbus-launch xvfb-run --server-args="-screen 0 640x480x16" test/run-tests.py +endif diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/lintian-overrides b/debian/source/lintian-overrides new file mode 100644 index 0000000..788161e --- /dev/null +++ b/debian/source/lintian-overrides @@ -0,0 +1,3 @@ +## Long line(s): +source-contains-prebuilt-javascript-object test/module/webpage/mousedoubleclick-event.js line length * +source-is-missing test/module/webpage/mousedoubleclick-event.js line length * diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..7d4a8e3 --- /dev/null +++ b/debian/watch @@ -0,0 +1,7 @@ +version=3 + +opts=\ +repacksuffix=+dfsg,\ +dversionmangle=s/\+dfsg\d*//,\ + https://github.com/ariya/phantomjs/releases \ + .*/archive/v?\.?(\d[\d\.]+)\.tar\.gz diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..3c21965 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,2 @@ +.vagrant +/brandelf diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..7f23b7d --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,44 @@ +Packaging PhantomJS +=================== + +This directory contains various scripts to assist with making PhantomJS +packages. + +Packaging for Linux +------------------- + +Linux building/packaging is best done in a container to ensure +isolation. We use [Docker](https://www.docker.com/) to automate the +process. Please see the [Docker documentation](https://docs.docker.com/) +for instructions on installing Docker. For OS X or Windows host, +please use [Docker Toolbox](https://www.docker.com/docker-toolbox). + +Once you have Docker installed, run these commands from the top level +of the PhantomJS source repository: + +```bash + $ git clean -xfd . + $ docker run -v $PWD:/src debian:wheezy /src/deploy/docker-build.sh +``` + +For the 32-bit version: + +```bash + $ git clean -xfd . + $ docker run -v $PWD:/src tubia/debian:wheezy /src/deploy/docker-build.sh +``` + +The built binary will be extracted out of the container and copied to +the current directory. + + +Packaging for OS X +------------------ + +Run `deploy/build-and-package.sh`. That's it. + +However, if you have previously built the sources in release mode, you +should clean your tree to make sure all the debugging symbols gets +compiled: + + $ make clean && cd src/qt && make clean && cd ../.. diff --git a/deploy/brandelf.c b/deploy/brandelf.c new file mode 100644 index 0000000..530297b --- /dev/null +++ b/deploy/brandelf.c @@ -0,0 +1,212 @@ +/*- + * Copyright (c) 2000, 2001 David O'Brien + * Copyright (c) 1996 Søren Schmidt + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer + * in this position and unchanged. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +//NOTE: commented out to make it compile on linux +// __FBSDID("$FreeBSD: src/usr.bin/brandelf/brandelf.c,v 1.25.22.2 2012/03/16 03:22:37 eadler Exp $"); + +#include +//NOTE: changed path to make it compile on linux +#include +#include +#include +#include +#include +#include +#include +#include + +static int elftype(const char *); +static const char *iselftype(int); +static void printelftypes(void); +static void usage(void); + +struct ELFtypes { + const char *str; + int value; +}; +/* XXX - any more types? */ +static struct ELFtypes elftypes[] = { + { "FreeBSD", ELFOSABI_FREEBSD }, + { "Linux", ELFOSABI_LINUX }, + { "Solaris", ELFOSABI_SOLARIS }, + { "SVR4", ELFOSABI_SYSV } +}; + +int +main(int argc, char **argv) +{ + + const char *strtype = "FreeBSD"; + int type = ELFOSABI_FREEBSD; + int retval = 0; + int ch, change = 0, force = 0, listed = 0; + + while ((ch = getopt(argc, argv, "f:lt:v")) != -1) + switch (ch) { + case 'f': + if (change) + errx(1, "f option incompatible with t option"); + force = 1; + type = atoi(optarg); + if (errno == ERANGE || type < 0 || type > 255) { + warnx("invalid argument to option f: %s", + optarg); + usage(); + } + break; + case 'l': + printelftypes(); + listed = 1; + break; + case 'v': + /* does nothing */ + break; + case 't': + if (force) + errx(1, "t option incompatible with f option"); + change = 1; + strtype = optarg; + break; + default: + usage(); + } + argc -= optind; + argv += optind; + if (!argc) { + if (listed) + exit(0); + else { + warnx("no file(s) specified"); + usage(); + } + } + + if (!force && (type = elftype(strtype)) == -1) { + warnx("invalid ELF type '%s'", strtype); + printelftypes(); + usage(); + } + + while (argc) { + int fd; + char buffer[EI_NIDENT]; + + if ((fd = open(argv[0], change || force ? O_RDWR : O_RDONLY, 0)) < 0) { + warn("error opening file %s", argv[0]); + retval = 1; + goto fail; + } + if (read(fd, buffer, EI_NIDENT) < EI_NIDENT) { + warnx("file '%s' too short", argv[0]); + retval = 1; + goto fail; + } + if (buffer[0] != ELFMAG0 || buffer[1] != ELFMAG1 || + buffer[2] != ELFMAG2 || buffer[3] != ELFMAG3) { + warnx("file '%s' is not ELF format", argv[0]); + retval = 1; + goto fail; + } + if (!change && !force) { + fprintf(stdout, + "File '%s' is of brand '%s' (%u).\n", + argv[0], iselftype(buffer[EI_OSABI]), + buffer[EI_OSABI]); + if (!iselftype(type)) { + warnx("ELF ABI Brand '%u' is unknown", + type); + printelftypes(); + } + } + else { + buffer[EI_OSABI] = type; + lseek(fd, 0, SEEK_SET); + if (write(fd, buffer, EI_NIDENT) != EI_NIDENT) { + warn("error writing %s %d", argv[0], fd); + retval = 1; + goto fail; + } + } +fail: + close(fd); + argc--; + argv++; + } + + return retval; +} + +static void +usage(void) +{ + (void)fprintf(stderr, + "usage: brandelf [-lv] [-f ELF_ABI_number] [-t string] file ...\n"); + exit(1); +} + +static const char * +iselftype(int etype) +{ + size_t elfwalk; + + for (elfwalk = 0; + elfwalk < sizeof(elftypes)/sizeof(elftypes[0]); + elfwalk++) + if (etype == elftypes[elfwalk].value) + return elftypes[elfwalk].str; + return 0; +} + +static int +elftype(const char *elfstrtype) +{ + size_t elfwalk; + + for (elfwalk = 0; + elfwalk < sizeof(elftypes)/sizeof(elftypes[0]); + elfwalk++) + if (strcasecmp(elfstrtype, elftypes[elfwalk].str) == 0) + return elftypes[elfwalk].value; + return -1; +} + +static void +printelftypes(void) +{ + size_t elfwalk; + + fprintf(stderr, "known ELF types are: "); + for (elfwalk = 0; + elfwalk < sizeof(elftypes)/sizeof(elftypes[0]); + elfwalk++) + fprintf(stderr, "%s(%u) ", elftypes[elfwalk].str, + elftypes[elfwalk].value); + fprintf(stderr, "\n"); +} diff --git a/deploy/build-and-package.sh b/deploy/build-and-package.sh new file mode 100755 index 0000000..d11501f --- /dev/null +++ b/deploy/build-and-package.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +cd `dirname $0`/.. + +./build.py --confirm --release --git-clean-qtbase --git-clean-qtwebkit "$@" || exit 1 + diff --git a/deploy/docker-build.sh b/deploy/docker-build.sh new file mode 100755 index 0000000..5ceb089 --- /dev/null +++ b/deploy/docker-build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -e + +SOURCE_PATH=/src +BUILD_PATH=$HOME/build + +# In case the old package URL is still being used +sed -i 's/http\.debian\.net/httpredir\.debian\.org/g' /etc/apt/sources.list + +echo "Installing packages for development tools..." && sleep 1 +apt-get -y update +apt-get install -y build-essential git flex bison gperf python ruby git libfontconfig1-dev +echo + +echo "Preparing to download Debian source package..." +echo "deb-src http://httpredir.debian.org/debian wheezy main" >> /etc/apt/sources.list +apt-get -y update +echo + +OPENSSL_TARGET='linux-x86_64' +if [ `getconf LONG_BIT` -eq 32 ]; then + OPENSSL_TARGET='linux-generic32' +fi +echo "Recompiling OpenSSL for ${OPENSSL_TARGET}..." && sleep 1 +apt-get source openssl +cd openssl-1.0.1e +OPENSSL_FLAGS='no-idea no-mdc2 no-rc5 no-zlib enable-tlsext no-ssl2 no-ssl3 no-ssl3-method enable-rfc3779 enable-cms' +./Configure --prefix=/usr --openssldir=/etc/ssl --libdir=lib ${OPENSSL_FLAGS} ${OPENSSL_TARGET} +make depend && make && make install +cd .. +echo + +echo "Building the static version of ICU library..." && sleep 1 +apt-get source icu +cd icu-4.8.1.1/source +./configure --prefix=/usr --enable-static --disable-shared +make && make install +cd .. +echo + +echo "Recreating the build directory $BUILD_PATH..." +rm -rf $BUILD_PATH && mkdir -p $BUILD_PATH +echo + +echo "Transferring the source: $SOURCE_PATH -> $BUILD_PATH. Please wait..." +cd $BUILD_PATH && cp -rp $SOURCE_PATH . && cd src +echo + +echo "Compiling PhantomJS..." && sleep 1 +python build.py --confirm --release --qt-config="-no-pkg-config" --git-clean-qtbase --git-clean-qtwebkit +echo + +echo "Stripping the executable..." && sleep 1 +ls -l bin/phantomjs +strip bin/phantomjs +echo "Copying the executable..." && sleep 1 +ls -l bin/phantomjs +cp bin/phantomjs $SOURCE_PATH +echo + +echo "Finished." diff --git a/deploy/package.sh b/deploy/package.sh new file mode 100755 index 0000000..16d6525 --- /dev/null +++ b/deploy/package.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +# +# usage: just run this script (after having run build.sh) +# and deploy the created tarball to your target machine. +# +# It creates a phantomjs-$version folder and copies the binary, +# example, license etc. together with all shared library dependencies +# to that folder. Furthermore brandelf is used to make the lib +# and binary compatible with older unix/linux machines that don't +# know the new Linux ELF ABI. +# + +cd $(dirname $0) + +if [[ ! -f ../bin/phantomjs ]]; then + echo "phantomjs was not built yet, please run build.sh first" + exit 1 +fi + +if [[ "$1" = "--bundle-libs" ]]; then + bundle_libs=1 +else + bundle_libs=0 +fi + +version=$(../bin/phantomjs --version | sed 's/ /-/' | sed 's/[()]//g') +src=.. + +echo "packaging phantomjs $version" + +if [[ $OSTYPE = darwin* ]]; then + dest="phantomjs-$version-macosx" +else + dest="phantomjs-$version-linux-$(uname -m)" +fi + +rm -Rf $dest{.tar.bz2,} &> /dev/null +mkdir -p $dest/bin + +echo + +echo -n "copying files..." +cp $src/bin/phantomjs $dest/bin +cp -r $src/{ChangeLog,examples,LICENSE.BSD,third-party.txt,README.md} $dest/ +echo "done" +echo + +phantomjs=$dest/bin/phantomjs + +if [[ "$bundle_libs" = "1" ]]; then + mkdir -p $dest/lib + + if [[ ! -f brandelf ]]; then + echo + echo "brandelf executable not found in current dir" + echo -n "compiling it now..." + g++ brandelf.c -o brandelf || exit 1 + echo "done" + fi + + libs=$(ldd $phantomjs | egrep -o "/[^ ]+ ") + + echo -n "copying shared libs..." + libld= + for l in $libs; do + ll=$(basename $l) + cp $l $dest/lib/$ll + + if [[ "$bundle_libs" = "1" ]]; then + # ensure OS ABI compatibility + ./brandelf -t SVR4 $dest/lib/$ll + if [[ "$l" == *"ld-linux"* ]]; then + libld=$ll + fi + fi + done + echo "done" + echo + + echo -n "writing run script..." + mv $phantomjs $phantomjs.bin + phantomjs=$phantomjs.bin + run=$dest/bin/phantomjs + echo '#!/bin/sh' >> $run + echo 'path=$(dirname $(dirname $(readlink -f $0)))' >> $run + echo 'export LD_LIBRARY_PATH=$path/lib' >> $run + echo 'exec $path/lib/'$libld' $phantomjs $@' >> $run + chmod +x $run + echo "done" + echo +fi + +echo -n "stripping binary and libs..." +if [[ $OSTYPE = darwin* ]]; then + strip -x $phantomjs +else + strip -s $phantomjs + [[ -d $dest/lib ]] && strip -s $dest/lib/* +fi +echo "done" +echo + +echo -n "compressing binary..." +if type upx >/dev/null 2>&1; then + upx -qqq -9 $phantomjs + echo "done" +else + echo "upx not found" +fi +echo + +echo -n "creating archive..." +if [[ $OSTYPE = darwin* ]]; then + zip -r $dest.zip $dest +else + tar -cjf $dest{.tar.bz2,} +fi +echo "done" +echo diff --git a/examples/arguments.js b/examples/arguments.js new file mode 100644 index 0000000..9c75974 --- /dev/null +++ b/examples/arguments.js @@ -0,0 +1,10 @@ +"use strict"; +var system = require('system'); +if (system.args.length === 1) { + console.log('Try to pass some args when invoking this script!'); +} else { + system.args.forEach(function (arg, i) { + console.log(i + ': ' + arg); + }); +} +phantom.exit(); diff --git a/examples/child_process-examples.js b/examples/child_process-examples.js new file mode 100644 index 0000000..19f9199 --- /dev/null +++ b/examples/child_process-examples.js @@ -0,0 +1,28 @@ +"use strict"; +var spawn = require("child_process").spawn +var execFile = require("child_process").execFile + +var child = spawn("ls", ["-lF", "/rooot"]) + +child.stdout.on("data", function (data) { + console.log("spawnSTDOUT:", JSON.stringify(data)) +}) + +child.stderr.on("data", function (data) { + console.log("spawnSTDERR:", JSON.stringify(data)) +}) + +child.on("exit", function (code) { + console.log("spawnEXIT:", code) +}) + +//child.kill("SIGKILL") + +execFile("ls", ["-lF", "/usr"], null, function (err, stdout, stderr) { + console.log("execFileSTDOUT:", JSON.stringify(stdout)) + console.log("execFileSTDERR:", JSON.stringify(stderr)) +}) + +setTimeout(function () { + phantom.exit(0) +}, 2000) diff --git a/examples/colorwheel.js b/examples/colorwheel.js new file mode 100644 index 0000000..393e9e9 --- /dev/null +++ b/examples/colorwheel.js @@ -0,0 +1,52 @@ +"use strict"; +var page = require('webpage').create(); +page.viewportSize = { width: 400, height : 400 }; +page.content = ''; +page.evaluate(function() { + var el = document.getElementById('surface'), + context = el.getContext('2d'), + width = window.innerWidth, + height = window.innerHeight, + cx = width / 2, + cy = height / 2, + radius = width / 2.3, + imageData, + pixels, + hue, sat, value, + i = 0, x, y, rx, ry, d, + f, g, p, u, v, w, rgb; + + el.width = width; + el.height = height; + imageData = context.createImageData(width, height); + pixels = imageData.data; + + for (y = 0; y < height; y = y + 1) { + for (x = 0; x < width; x = x + 1, i = i + 4) { + rx = x - cx; + ry = y - cy; + d = rx * rx + ry * ry; + if (d < radius * radius) { + hue = 6 * (Math.atan2(ry, rx) + Math.PI) / (2 * Math.PI); + sat = Math.sqrt(d) / radius; + g = Math.floor(hue); + f = hue - g; + u = 255 * (1 - sat); + v = 255 * (1 - sat * f); + w = 255 * (1 - sat * (1 - f)); + pixels[i] = [255, v, u, u, w, 255, 255][g]; + pixels[i + 1] = [w, 255, 255, v, u, u, w][g]; + pixels[i + 2] = [u, u, w, 255, 255, v, u][g]; + pixels[i + 3] = 255; + } + } + } + + context.putImageData(imageData, 0, 0); + document.body.style.backgroundColor = 'white'; + document.body.style.margin = '0px'; +}); + +page.render('colorwheel.png'); + +phantom.exit(); diff --git a/examples/countdown.js b/examples/countdown.js new file mode 100644 index 0000000..4dfd818 --- /dev/null +++ b/examples/countdown.js @@ -0,0 +1,10 @@ +"use strict"; +var t = 10, + interval = setInterval(function(){ + if ( t > 0 ) { + console.log(t--); + } else { + console.log("BLAST OFF!"); + phantom.exit(); + } + }, 1000); diff --git a/examples/detectsniff.js b/examples/detectsniff.js new file mode 100644 index 0000000..f30867b --- /dev/null +++ b/examples/detectsniff.js @@ -0,0 +1,60 @@ +// Detect if a web page sniffs the user agent or not. + +"use strict"; +var page = require('webpage').create(), + system = require('system'), + sniffed, + address; + +page.onInitialized = function () { + page.evaluate(function () { + + (function () { + var userAgent = window.navigator.userAgent, + platform = window.navigator.platform; + + window.navigator = { + appCodeName: 'Mozilla', + appName: 'Netscape', + cookieEnabled: false, + sniffed: false + }; + + window.navigator.__defineGetter__('userAgent', function () { + window.navigator.sniffed = true; + return userAgent; + }); + + window.navigator.__defineGetter__('platform', function () { + window.navigator.sniffed = true; + return platform; + }); + })(); + }); +}; + +if (system.args.length === 1) { + console.log('Usage: detectsniff.js '); + phantom.exit(1); +} else { + address = system.args[1]; + console.log('Checking ' + address + '...'); + page.open(address, function (status) { + if (status !== 'success') { + console.log('FAIL to load the address'); + phantom.exit(); + } else { + window.setTimeout(function () { + sniffed = page.evaluate(function () { + return navigator.sniffed; + }); + if (sniffed) { + console.log('The page tried to sniff the user agent.'); + } else { + console.log('The page did not try to sniff the user agent.'); + } + phantom.exit(); + }, 1500); + } + }); +} diff --git a/examples/echoToFile.js b/examples/echoToFile.js new file mode 100644 index 0000000..cfb8858 --- /dev/null +++ b/examples/echoToFile.js @@ -0,0 +1,24 @@ +// echoToFile.js - Write in a given file all the parameters passed on the CLI +"use strict"; +var fs = require('fs'), + system = require('system'); + +if (system.args.length < 3) { + console.log("Usage: echoToFile.js DESTINATION_FILE "); + phantom.exit(1); +} else { + var content = '', + f = null, + i; + for ( i= 2; i < system.args.length; ++i ) { + content += system.args[i] + (i === system.args.length-1 ? '' : ' '); + } + + try { + fs.write(system.args[1], content, 'w'); + } catch(e) { + console.log(e); + } + + phantom.exit(); +} diff --git a/examples/features.js b/examples/features.js new file mode 100644 index 0000000..4c2a14a --- /dev/null +++ b/examples/features.js @@ -0,0 +1,30 @@ +"use strict"; +var feature, supported = [], unsupported = []; + +phantom.injectJs('modernizr.js'); +console.log('Detected features (using Modernizr ' + Modernizr._version + '):'); +for (feature in Modernizr) { + if (Modernizr.hasOwnProperty(feature)) { + if (feature[0] !== '_' && typeof Modernizr[feature] !== 'function' && + feature !== 'input' && feature !== 'inputtypes') { + if (Modernizr[feature]) { + supported.push(feature); + } else { + unsupported.push(feature); + } + } + } +} + +console.log(''); +console.log('Supported:'); +supported.forEach(function (e) { + console.log(' ' + e); +}); + +console.log(''); +console.log('Not supported:'); +unsupported.forEach(function (e) { + console.log(' ' + e); +}); +phantom.exit(); diff --git a/examples/fibo.js b/examples/fibo.js new file mode 100644 index 0000000..7ee472a --- /dev/null +++ b/examples/fibo.js @@ -0,0 +1,10 @@ +"use strict"; +var fibs = [0, 1]; +var ticker = window.setInterval(function () { + console.log(fibs[fibs.length - 1]); + fibs.push(fibs[fibs.length - 1] + fibs[fibs.length - 2]); + if (fibs.length > 10) { + window.clearInterval(ticker); + phantom.exit(); + } +}, 300); diff --git a/examples/hello.js b/examples/hello.js new file mode 100644 index 0000000..82d2dbb --- /dev/null +++ b/examples/hello.js @@ -0,0 +1,3 @@ +"use strict"; +console.log('Hello, world!'); +phantom.exit(); diff --git a/examples/injectme.js b/examples/injectme.js new file mode 100644 index 0000000..18d9fda --- /dev/null +++ b/examples/injectme.js @@ -0,0 +1,26 @@ +// Use 'page.injectJs()' to load the script itself in the Page context + +"use strict"; +if ( typeof(phantom) !== "undefined" ) { + var page = require('webpage').create(); + + // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onAlert = function(msg) { + console.log(msg); + }; + + console.log("* Script running in the Phantom context."); + console.log("* Script will 'inject' itself in a page..."); + page.open("about:blank", function(status) { + if ( status === "success" ) { + console.log(page.injectJs("injectme.js") ? "... done injecting itself!" : "... fail! Check the $PWD?!"); + } + phantom.exit(); + }); +} else { + alert("* Script running in the Page context."); +} diff --git a/examples/loadspeed.js b/examples/loadspeed.js new file mode 100644 index 0000000..626a51c --- /dev/null +++ b/examples/loadspeed.js @@ -0,0 +1,24 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'), + t, address; + +if (system.args.length === 1) { + console.log('Usage: loadspeed.js '); + phantom.exit(1); +} else { + t = Date.now(); + address = system.args[1]; + page.open(address, function (status) { + if (status !== 'success') { + console.log('FAIL to load the address'); + } else { + t = Date.now() - t; + console.log('Page title is ' + page.evaluate(function () { + return document.title; + })); + console.log('Loading time ' + t + ' msec'); + } + phantom.exit(); + }); +} diff --git a/examples/loadurlwithoutcss.js b/examples/loadurlwithoutcss.js new file mode 100644 index 0000000..12fc28a --- /dev/null +++ b/examples/loadurlwithoutcss.js @@ -0,0 +1,26 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'); + +if (system.args.length < 2) { + console.log('Usage: loadurlwithoutcss.js URL'); + phantom.exit(); +} + +var address = system.args[1]; + +page.onResourceRequested = function(requestData, request) { + if ((/http:\/\/.+?\.css/gi).test(requestData['url']) || requestData.headers['Content-Type'] == 'text/css') { + console.log('The url of the request is matching. Aborting: ' + requestData['url']); + request.abort(); + } +}; + +page.open(address, function(status) { + if (status === 'success') { + phantom.exit(); + } else { + console.log('Unable to load the address!'); + phantom.exit(); + } +}); \ No newline at end of file diff --git a/examples/modernizr.js b/examples/modernizr.js new file mode 100644 index 0000000..24de6cc --- /dev/null +++ b/examples/modernizr.js @@ -0,0 +1,1406 @@ +/*! + * Modernizr v2.8.2 + * www.modernizr.com + * + * Copyright (c) Faruk Ates, Paul Irish, Alex Sexton + * Available under the BSD and MIT licenses: www.modernizr.com/license/ + */ + +/* + * Modernizr tests which native CSS3 and HTML5 features are available in + * the current UA and makes the results available to you in two ways: + * as properties on a global Modernizr object, and as classes on the + * element. This information allows you to progressively enhance + * your pages with a granular level of control over the experience. + * + * Modernizr has an optional (not included) conditional resource loader + * called Modernizr.load(), based on Yepnope.js (yepnopejs.com). + * To get a build that includes Modernizr.load(), as well as choosing + * which tests to include, go to www.modernizr.com/download/ + * + * Authors Faruk Ates, Paul Irish, Alex Sexton + * Contributors Ryan Seddon, Ben Alman + */ + +window.Modernizr = (function( window, document, undefined ) { + + var version = '2.8.2', + + Modernizr = {}, + + /*>>cssclasses*/ + // option for enabling the HTML classes to be added + enableClasses = true, + /*>>cssclasses*/ + + docElement = document.documentElement, + + /** + * Create our "modernizr" element that we do most feature tests on. + */ + mod = 'modernizr', + modElem = document.createElement(mod), + mStyle = modElem.style, + + /** + * Create the input element for various Web Forms feature tests. + */ + inputElem /*>>inputelem*/ = document.createElement('input') /*>>inputelem*/ , + + /*>>smile*/ + smile = ':)', + /*>>smile*/ + + toString = {}.toString, + + // TODO :: make the prefixes more granular + /*>>prefixes*/ + // List of property values to set for css tests. See ticket #21 + prefixes = ' -webkit- -moz- -o- -ms- '.split(' '), + /*>>prefixes*/ + + /*>>domprefixes*/ + // Following spec is to expose vendor-specific style properties as: + // elem.style.WebkitBorderRadius + // and the following would be incorrect: + // elem.style.webkitBorderRadius + + // Webkit ghosts their properties in lowercase but Opera & Moz do not. + // Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+ + // erik.eae.net/archives/2008/03/10/21.48.10/ + + // More here: github.com/Modernizr/Modernizr/issues/issue/21 + omPrefixes = 'Webkit Moz O ms', + + cssomPrefixes = omPrefixes.split(' '), + + domPrefixes = omPrefixes.toLowerCase().split(' '), + /*>>domprefixes*/ + + /*>>ns*/ + ns = {'svg': 'http://www.w3.org/2000/svg'}, + /*>>ns*/ + + tests = {}, + inputs = {}, + attrs = {}, + + classes = [], + + slice = classes.slice, + + featureName, // used in testing loop + + + /*>>teststyles*/ + // Inject element with style element and some CSS rules + injectElementWithStyles = function( rule, callback, nodes, testnames ) { + + var style, ret, node, docOverflow, + div = document.createElement('div'), + // After page load injecting a fake body doesn't work so check if body exists + body = document.body, + // IE6 and 7 won't return offsetWidth or offsetHeight unless it's in the body element, so we fake it. + fakeBody = body || document.createElement('body'); + + if ( parseInt(nodes, 10) ) { + // In order not to give false positives we create a node for each test + // This also allows the method to scale for unspecified uses + while ( nodes-- ) { + node = document.createElement('div'); + node.id = testnames ? testnames[nodes] : mod + (nodes + 1); + div.appendChild(node); + } + } + + // '].join(''); + div.id = mod; + // IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody. + // Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270 + (body ? div : fakeBody).innerHTML += style; + fakeBody.appendChild(div); + if ( !body ) { + //avoid crashing IE8, if background image is used + fakeBody.style.background = ''; + //Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible + fakeBody.style.overflow = 'hidden'; + docOverflow = docElement.style.overflow; + docElement.style.overflow = 'hidden'; + docElement.appendChild(fakeBody); + } + + ret = callback(div, rule); + // If this is done after page load we don't want to remove the body so check if body exists + if ( !body ) { + fakeBody.parentNode.removeChild(fakeBody); + docElement.style.overflow = docOverflow; + } else { + div.parentNode.removeChild(div); + } + + return !!ret; + + }, + /*>>teststyles*/ + + /*>>mq*/ + // adapted from matchMedia polyfill + // by Scott Jehl and Paul Irish + // gist.github.com/786768 + testMediaQuery = function( mq ) { + + var matchMedia = window.matchMedia || window.msMatchMedia; + if ( matchMedia ) { + return matchMedia(mq) && matchMedia(mq).matches || false; + } + + var bool; + + injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) { + bool = (window.getComputedStyle ? + getComputedStyle(node, null) : + node.currentStyle)['position'] == 'absolute'; + }); + + return bool; + + }, + /*>>mq*/ + + + /*>>hasevent*/ + // + // isEventSupported determines if a given element supports the given event + // kangax.github.com/iseventsupported/ + // + // The following results are known incorrects: + // Modernizr.hasEvent("webkitTransitionEnd", elem) // false negative + // Modernizr.hasEvent("textInput") // in Webkit. github.com/Modernizr/Modernizr/issues/333 + // ... + isEventSupported = (function() { + + var TAGNAMES = { + 'select': 'input', 'change': 'input', + 'submit': 'form', 'reset': 'form', + 'error': 'img', 'load': 'img', 'abort': 'img' + }; + + function isEventSupported( eventName, element ) { + + element = element || document.createElement(TAGNAMES[eventName] || 'div'); + eventName = 'on' + eventName; + + // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those + var isSupported = eventName in element; + + if ( !isSupported ) { + // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element + if ( !element.setAttribute ) { + element = document.createElement('div'); + } + if ( element.setAttribute && element.removeAttribute ) { + element.setAttribute(eventName, ''); + isSupported = is(element[eventName], 'function'); + + // If property was created, "remove it" (by setting value to `undefined`) + if ( !is(element[eventName], 'undefined') ) { + element[eventName] = undefined; + } + element.removeAttribute(eventName); + } + } + + element = null; + return isSupported; + } + return isEventSupported; + })(), + /*>>hasevent*/ + + // TODO :: Add flag for hasownprop ? didn't last time + + // hasOwnProperty shim by kangax needed for Safari 2.0 support + _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp; + + if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) { + hasOwnProp = function (object, property) { + return _hasOwnProperty.call(object, property); + }; + } + else { + hasOwnProp = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */ + return ((property in object) && is(object.constructor.prototype[property], 'undefined')); + }; + } + + // Adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js + // es5.github.com/#x15.3.4.5 + + if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { + + var target = this; + + if (typeof target != "function") { + throw new TypeError(); + } + + var args = slice.call(arguments, 1), + bound = function () { + + if (this instanceof bound) { + + var F = function(){}; + F.prototype = target.prototype; + var self = new F(); + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return self; + + } else { + + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + + return bound; + }; + } + + /** + * setCss applies given styles to the Modernizr DOM node. + */ + function setCss( str ) { + mStyle.cssText = str; + } + + /** + * setCssAll extrapolates all vendor-specific css strings. + */ + function setCssAll( str1, str2 ) { + return setCss(prefixes.join(str1 + ';') + ( str2 || '' )); + } + + /** + * is returns a boolean for if typeof obj is exactly type. + */ + function is( obj, type ) { + return typeof obj === type; + } + + /** + * contains returns a boolean for if substr is found within str. + */ + function contains( str, substr ) { + return !!~('' + str).indexOf(substr); + } + + /*>>testprop*/ + + // testProps is a generic CSS / DOM property test. + + // In testing support for a given CSS property, it's legit to test: + // `elem.style[styleName] !== undefined` + // If the property is supported it will return an empty string, + // if unsupported it will return undefined. + + // We'll take advantage of this quick test and skip setting a style + // on our modernizr element, but instead just testing undefined vs + // empty string. + + // Because the testing of the CSS property names (with "-", as + // opposed to the camelCase DOM properties) is non-portable and + // non-standard but works in WebKit and IE (but not Gecko or Opera), + // we explicitly reject properties with dashes so that authors + // developing in WebKit or IE first don't end up with + // browser-specific content by accident. + + function testProps( props, prefixed ) { + for ( var i in props ) { + var prop = props[i]; + if ( !contains(prop, "-") && mStyle[prop] !== undefined ) { + return prefixed == 'pfx' ? prop : true; + } + } + return false; + } + /*>>testprop*/ + + // TODO :: add testDOMProps + /** + * testDOMProps is a generic DOM property test; if a browser supports + * a certain property, it won't return undefined for it. + */ + function testDOMProps( props, obj, elem ) { + for ( var i in props ) { + var item = obj[props[i]]; + if ( item !== undefined) { + + // return the property name as a string + if (elem === false) return props[i]; + + // let's bind a function + if (is(item, 'function')){ + // default to autobind unless override + return item.bind(elem || obj); + } + + // return the unbound function or obj or value + return item; + } + } + return false; + } + + /*>>testallprops*/ + /** + * testPropsAll tests a list of DOM properties we want to check against. + * We specify literally ALL possible (known and/or likely) properties on + * the element including the non-vendor prefixed one, for forward- + * compatibility. + */ + function testPropsAll( prop, prefixed, elem ) { + + var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), + props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' '); + + // did they call .prefixed('boxSizing') or are we just testing a prop? + if(is(prefixed, "string") || is(prefixed, "undefined")) { + return testProps(props, prefixed); + + // otherwise, they called .prefixed('requestAnimationFrame', window[, elem]) + } else { + props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' '); + return testDOMProps(props, prefixed, elem); + } + } + /*>>testallprops*/ + + + /** + * Tests + * ----- + */ + + // The *new* flexbox + // dev.w3.org/csswg/css3-flexbox + + tests['flexbox'] = function() { + return testPropsAll('flexWrap'); + }; + + // The *old* flexbox + // www.w3.org/TR/2009/WD-css3-flexbox-20090723/ + + tests['flexboxlegacy'] = function() { + return testPropsAll('boxDirection'); + }; + + // On the S60 and BB Storm, getContext exists, but always returns undefined + // so we actually have to call getContext() to verify + // github.com/Modernizr/Modernizr/issues/issue/97/ + + tests['canvas'] = function() { + var elem = document.createElement('canvas'); + return !!(elem.getContext && elem.getContext('2d')); + }; + + tests['canvastext'] = function() { + return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function')); + }; + + // webk.it/70117 is tracking a legit WebGL feature detect proposal + + // We do a soft detect which may false positive in order to avoid + // an expensive context creation: bugzil.la/732441 + + tests['webgl'] = function() { + return !!window.WebGLRenderingContext; + }; + + /* + * The Modernizr.touch test only indicates if the browser supports + * touch events, which does not necessarily reflect a touchscreen + * device, as evidenced by tablets running Windows 7 or, alas, + * the Palm Pre / WebOS (touch) phones. + * + * Additionally, Chrome (desktop) used to lie about its support on this, + * but that has since been rectified: crbug.com/36415 + * + * We also test for Firefox 4 Multitouch Support. + * + * For more info, see: modernizr.github.com/Modernizr/touch.html + */ + + tests['touch'] = function() { + var bool; + + if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + bool = true; + } else { + injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) { + bool = node.offsetTop === 9; + }); + } + + return bool; + }; + + + // geolocation is often considered a trivial feature detect... + // Turns out, it's quite tricky to get right: + // + // Using !!navigator.geolocation does two things we don't want. It: + // 1. Leaks memory in IE9: github.com/Modernizr/Modernizr/issues/513 + // 2. Disables page caching in WebKit: webk.it/43956 + // + // Meanwhile, in Firefox < 8, an about:config setting could expose + // a false positive that would throw an exception: bugzil.la/688158 + + tests['geolocation'] = function() { + return 'geolocation' in navigator; + }; + + + tests['postmessage'] = function() { + return !!window.postMessage; + }; + + + // Chrome incognito mode used to throw an exception when using openDatabase + // It doesn't anymore. + tests['websqldatabase'] = function() { + return !!window.openDatabase; + }; + + // Vendors had inconsistent prefixing with the experimental Indexed DB: + // - Webkit's implementation is accessible through webkitIndexedDB + // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB + // For speed, we don't test the legacy (and beta-only) indexedDB + tests['indexedDB'] = function() { + return !!testPropsAll("indexedDB", window); + }; + + // documentMode logic from YUI to filter out IE8 Compat Mode + // which false positives. + tests['hashchange'] = function() { + return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7); + }; + + // Per 1.6: + // This used to be Modernizr.historymanagement but the longer + // name has been deprecated in favor of a shorter and property-matching one. + // The old API is still available in 1.6, but as of 2.0 will throw a warning, + // and in the first release thereafter disappear entirely. + tests['history'] = function() { + return !!(window.history && history.pushState); + }; + + tests['draganddrop'] = function() { + var div = document.createElement('div'); + return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div); + }; + + // FF3.6 was EOL'ed on 4/24/12, but the ESR version of FF10 + // will be supported until FF19 (2/12/13), at which time, ESR becomes FF17. + // FF10 still uses prefixes, so check for it until then. + // for more ESR info, see: mozilla.org/en-US/firefox/organizations/faq/ + tests['websockets'] = function() { + return 'WebSocket' in window || 'MozWebSocket' in window; + }; + + + // css-tricks.com/rgba-browser-support/ + tests['rgba'] = function() { + // Set an rgba() color and check the returned value + + setCss('background-color:rgba(150,255,150,.5)'); + + return contains(mStyle.backgroundColor, 'rgba'); + }; + + tests['hsla'] = function() { + // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally, + // except IE9 who retains it as hsla + + setCss('background-color:hsla(120,40%,100%,.5)'); + + return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla'); + }; + + tests['multiplebgs'] = function() { + // Setting multiple images AND a color on the background shorthand property + // and then querying the style.background property value for the number of + // occurrences of "url(" is a reliable method for detecting ACTUAL support for this! + + setCss('background:url(https://),url(https://),red url(https://)'); + + // If the UA supports multiple backgrounds, there should be three occurrences + // of the string "url(" in the return value for elemStyle.background + + return (/(url\s*\(.*?){3}/).test(mStyle.background); + }; + + + + // this will false positive in Opera Mini + // github.com/Modernizr/Modernizr/issues/396 + + tests['backgroundsize'] = function() { + return testPropsAll('backgroundSize'); + }; + + tests['borderimage'] = function() { + return testPropsAll('borderImage'); + }; + + + // Super comprehensive table about all the unique implementations of + // border-radius: muddledramblings.com/table-of-css3-border-radius-compliance + + tests['borderradius'] = function() { + return testPropsAll('borderRadius'); + }; + + // WebOS unfortunately false positives on this test. + tests['boxshadow'] = function() { + return testPropsAll('boxShadow'); + }; + + // FF3.0 will false positive on this test + tests['textshadow'] = function() { + return document.createElement('div').style.textShadow === ''; + }; + + + tests['opacity'] = function() { + // Browsers that actually have CSS Opacity implemented have done so + // according to spec, which means their return values are within the + // range of [0.0,1.0] - including the leading zero. + + setCssAll('opacity:.55'); + + // The non-literal . in this regex is intentional: + // German Chrome returns this value as 0,55 + // github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632 + return (/^0.55$/).test(mStyle.opacity); + }; + + + // Note, Android < 4 will pass this test, but can only animate + // a single property at a time + // goo.gl/v3V4Gp + tests['cssanimations'] = function() { + return testPropsAll('animationName'); + }; + + + tests['csscolumns'] = function() { + return testPropsAll('columnCount'); + }; + + + tests['cssgradients'] = function() { + /** + * For CSS Gradients syntax, please see: + * webkit.org/blog/175/introducing-css-gradients/ + * developer.mozilla.org/en/CSS/-moz-linear-gradient + * developer.mozilla.org/en/CSS/-moz-radial-gradient + * dev.w3.org/csswg/css3-images/#gradients- + */ + + var str1 = 'background-image:', + str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));', + str3 = 'linear-gradient(left top,#9f9, white);'; + + setCss( + // legacy webkit syntax (FIXME: remove when syntax not in use anymore) + (str1 + '-webkit- '.split(' ').join(str2 + str1) + + // standard syntax // trailing 'background-image:' + prefixes.join(str3 + str1)).slice(0, -str1.length) + ); + + return contains(mStyle.backgroundImage, 'gradient'); + }; + + + tests['cssreflections'] = function() { + return testPropsAll('boxReflect'); + }; + + + tests['csstransforms'] = function() { + return !!testPropsAll('transform'); + }; + + + tests['csstransforms3d'] = function() { + + var ret = !!testPropsAll('perspective'); + + // Webkit's 3D transforms are passed off to the browser's own graphics renderer. + // It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in + // some conditions. As a result, Webkit typically recognizes the syntax but + // will sometimes throw a false positive, thus we must do a more thorough check: + if ( ret && 'webkitPerspective' in docElement.style ) { + + // Webkit allows this media query to succeed only if the feature is enabled. + // `@media (transform-3d),(-webkit-transform-3d){ ... }` + injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) { + ret = node.offsetLeft === 9 && node.offsetHeight === 3; + }); + } + return ret; + }; + + + tests['csstransitions'] = function() { + return testPropsAll('transition'); + }; + + + /*>>fontface*/ + // @font-face detection routine by Diego Perini + // javascript.nwbox.com/CSSSupport/ + + // false positives: + // WebOS github.com/Modernizr/Modernizr/issues/342 + // WP7 github.com/Modernizr/Modernizr/issues/538 + tests['fontface'] = function() { + var bool; + + injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) { + var style = document.getElementById('smodernizr'), + sheet = style.sheet || style.styleSheet, + cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : ''; + + bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0; + }); + + return bool; + }; + /*>>fontface*/ + + // CSS generated content detection + tests['generatedcontent'] = function() { + var bool; + + injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) { + bool = node.offsetHeight >= 3; + }); + + return bool; + }; + + + + // These tests evaluate support of the video/audio elements, as well as + // testing what types of content they support. + // + // We're using the Boolean constructor here, so that we can extend the value + // e.g. Modernizr.video // true + // Modernizr.video.ogg // 'probably' + // + // Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845 + // thx to NielsLeenheer and zcorpan + + // Note: in some older browsers, "no" was a return value instead of empty string. + // It was live in FF3.5.0 and 3.5.1, but fixed in 3.5.2 + // It was also live in Safari 4.0.0 - 4.0.4, but fixed in 4.0.5 + + tests['video'] = function() { + var elem = document.createElement('video'), + bool = false; + + // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224 + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,''); + + // Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546 + bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,''); + + bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,''); + } + + } catch(e) { } + + return bool; + }; + + tests['audio'] = function() { + var elem = document.createElement('audio'), + bool = false; + + try { + if ( bool = !!elem.canPlayType ) { + bool = new Boolean(bool); + bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,''); + bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,''); + + // Mimetypes accepted: + // developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements + // bit.ly/iphoneoscodecs + bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,''); + bool.m4a = ( elem.canPlayType('audio/x-m4a;') || + elem.canPlayType('audio/aac;')) .replace(/^no$/,''); + } + } catch(e) { } + + return bool; + }; + + + // In FF4, if disabled, window.localStorage should === null. + + // Normally, we could not test that directly and need to do a + // `('localStorage' in window) && ` test first because otherwise Firefox will + // throw bugzil.la/365772 if cookies are disabled + + // Also in iOS5 Private Browsing mode, attempting to use localStorage.setItem + // will throw the exception: + // QUOTA_EXCEEDED_ERRROR DOM Exception 22. + // Peculiarly, getItem and removeItem calls do not throw. + + // Because we are forced to try/catch this, we'll go aggressive. + + // Just FWIW: IE8 Compat mode supports these features completely: + // www.quirksmode.org/dom/html5.html + // But IE8 doesn't support either with local files + + tests['localstorage'] = function() { + try { + localStorage.setItem(mod, mod); + localStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + + tests['sessionstorage'] = function() { + try { + sessionStorage.setItem(mod, mod); + sessionStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + + + tests['webworkers'] = function() { + return !!window.Worker; + }; + + + tests['applicationcache'] = function() { + return !!window.applicationCache; + }; + + + // Thanks to Erik Dahlstrom + tests['svg'] = function() { + return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect; + }; + + // specifically for SVG inline in HTML, not within XHTML + // test page: paulirish.com/demo/inline-svg + tests['inlinesvg'] = function() { + var div = document.createElement('div'); + div.innerHTML = ''; + return (div.firstChild && div.firstChild.namespaceURI) == ns.svg; + }; + + // SVG SMIL animation + tests['smil'] = function() { + return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate'))); + }; + + // This test is only for clip paths in SVG proper, not clip paths on HTML content + // demo: srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg + + // However read the comments to dig into applying SVG clippaths to HTML content here: + // github.com/Modernizr/Modernizr/issues/213#issuecomment-1149491 + tests['svgclippaths'] = function() { + return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath'))); + }; + + /*>>webforms*/ + // input features and input types go directly onto the ret object, bypassing the tests loop. + // Hold this guy to execute in a moment. + function webforms() { + /*>>input*/ + // Run through HTML5's new input attributes to see if the UA understands any. + // We're using f which is the element created early on + // Mike Taylr has created a comprehensive resource for testing these attributes + // when applied to all input types: + // miketaylr.com/code/input-type-attr.html + // spec: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary + + // Only input placeholder is tested while textarea's placeholder is not. + // Currently Safari 4 and Opera 11 have support only for the input placeholder + // Both tests are available in feature-detects/forms-placeholder.js + Modernizr['input'] = (function( props ) { + for ( var i = 0, len = props.length; i < len; i++ ) { + attrs[ props[i] ] = !!(props[i] in inputElem); + } + if (attrs.list){ + // safari false positive's on datalist: webk.it/74252 + // see also github.com/Modernizr/Modernizr/issues/146 + attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement); + } + return attrs; + })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' ')); + /*>>input*/ + + /*>>inputtypes*/ + // Run through HTML5's new input types to see if the UA understands any. + // This is put behind the tests runloop because it doesn't return a + // true/false like all the other tests; instead, it returns an object + // containing each input type with its corresponding true/false value + + // Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/ + Modernizr['inputtypes'] = (function(props) { + + for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) { + + inputElem.setAttribute('type', inputElemType = props[i]); + bool = inputElem.type !== 'text'; + + // We first check to see if the type we give it sticks.. + // If the type does, we feed it a textual value, which shouldn't be valid. + // If the value doesn't stick, we know there's input sanitization which infers a custom UI + if ( bool ) { + + inputElem.value = smile; + inputElem.style.cssText = 'position:absolute;visibility:hidden;'; + + if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) { + + docElement.appendChild(inputElem); + defaultView = document.defaultView; + + // Safari 2-4 allows the smiley as a value, despite making a slider + bool = defaultView.getComputedStyle && + defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' && + // Mobile android web browser has false positive, so must + // check the height to see if the widget is actually there. + (inputElem.offsetHeight !== 0); + + docElement.removeChild(inputElem); + + } else if ( /^(search|tel)$/.test(inputElemType) ){ + // Spec doesn't define any special parsing or detectable UI + // behaviors so we pass these through as true + + // Interestingly, opera fails the earlier test, so it doesn't + // even make it here. + + } else if ( /^(url|email)$/.test(inputElemType) ) { + // Real url and email support comes with prebaked validation. + bool = inputElem.checkValidity && inputElem.checkValidity() === false; + + } else { + // If the upgraded input compontent rejects the :) text, we got a winner + bool = inputElem.value != smile; + } + } + + inputs[ props[i] ] = !!bool; + } + return inputs; + })('search tel url email datetime date month week time datetime-local number range color'.split(' ')); + /*>>inputtypes*/ + } + /*>>webforms*/ + + + // End of test definitions + // ----------------------- + + + + // Run through all tests and detect their support in the current UA. + // todo: hypothetically we could be doing an array of tests and use a basic loop here. + for ( var feature in tests ) { + if ( hasOwnProp(tests, feature) ) { + // run the test, throw the return value into the Modernizr, + // then based on that boolean, define an appropriate className + // and push it into an array of classes we'll join later. + featureName = feature.toLowerCase(); + Modernizr[featureName] = tests[feature](); + + classes.push((Modernizr[featureName] ? '' : 'no-') + featureName); + } + } + + /*>>webforms*/ + // input tests need to run. + Modernizr.input || webforms(); + /*>>webforms*/ + + + /** + * addTest allows the user to define their own feature tests + * the result will be added onto the Modernizr object, + * as well as an appropriate className set on the html element + * + * @param feature - String naming the feature + * @param test - Function returning true if feature is supported, false if not + */ + Modernizr.addTest = function ( feature, test ) { + if ( typeof feature == 'object' ) { + for ( var key in feature ) { + if ( hasOwnProp( feature, key ) ) { + Modernizr.addTest( key, feature[ key ] ); + } + } + } else { + + feature = feature.toLowerCase(); + + if ( Modernizr[feature] !== undefined ) { + // we're going to quit if you're trying to overwrite an existing test + // if we were to allow it, we'd do this: + // var re = new RegExp("\\b(no-)?" + feature + "\\b"); + // docElement.className = docElement.className.replace( re, '' ); + // but, no rly, stuff 'em. + return Modernizr; + } + + test = typeof test == 'function' ? test() : test; + + if (typeof enableClasses !== "undefined" && enableClasses) { + docElement.className += ' ' + (test ? '' : 'no-') + feature; + } + Modernizr[feature] = test; + + } + + return Modernizr; // allow chaining. + }; + + + // Reset modElem.cssText to nothing to reduce memory footprint. + setCss(''); + modElem = inputElem = null; + + /*>>shiv*/ + /** + * @preserve HTML5 Shiv prev3.7.1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed + */ + ;(function(window, document) { + /*jshint evil:true */ + /** version */ + var version = '3.7.0'; + + /** Preset options */ + var options = window.html5 || {}; + + /** Used to skip problem elements */ + var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i; + + /** Not all elements can be cloned in IE **/ + var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i; + + /** Detect whether the browser supports default html5 styles */ + var supportsHtml5Styles; + + /** Name of the expando, to work with multiple documents or to re-shiv one document */ + var expando = '_html5shiv'; + + /** The id for the the documents expando */ + var expanID = 0; + + /** Cached data for each document */ + var expandoData = {}; + + /** Detect whether the browser supports unknown elements */ + var supportsUnknownElements; + + (function() { + try { + var a = document.createElement('a'); + a.innerHTML = ''; + //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles + supportsHtml5Styles = ('hidden' in a); + + supportsUnknownElements = a.childNodes.length == 1 || (function() { + // assign a false positive if unable to shiv + (document.createElement)('a'); + var frag = document.createDocumentFragment(); + return ( + typeof frag.cloneNode == 'undefined' || + typeof frag.createDocumentFragment == 'undefined' || + typeof frag.createElement == 'undefined' + ); + }()); + } catch(e) { + // assign a false positive if detection fails => unable to shiv + supportsHtml5Styles = true; + supportsUnknownElements = true; + } + + }()); + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a style sheet with the given CSS text and adds it to the document. + * @private + * @param {Document} ownerDocument The document. + * @param {String} cssText The CSS text. + * @returns {StyleSheet} The style element. + */ + function addStyleSheet(ownerDocument, cssText) { + var p = ownerDocument.createElement('p'), + parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement; + + p.innerHTML = 'x'; + return parent.insertBefore(p.lastChild, parent.firstChild); + } + + /** + * Returns the value of `html5.elements` as an array. + * @private + * @returns {Array} An array of shived element node names. + */ + function getElements() { + var elements = html5.elements; + return typeof elements == 'string' ? elements.split(' ') : elements; + } + + /** + * Returns the data associated to the given document + * @private + * @param {Document} ownerDocument The document. + * @returns {Object} An object of data. + */ + function getExpandoData(ownerDocument) { + var data = expandoData[ownerDocument[expando]]; + if (!data) { + data = {}; + expanID++; + ownerDocument[expando] = expanID; + expandoData[expanID] = data; + } + return data; + } + + /** + * returns a shived element for the given nodeName and document + * @memberOf html5 + * @param {String} nodeName name of the element + * @param {Document} ownerDocument The context document. + * @returns {Object} The shived element. + */ + function createElement(nodeName, ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createElement(nodeName); + } + if (!data) { + data = getExpandoData(ownerDocument); + } + var node; + + if (data.cache[nodeName]) { + node = data.cache[nodeName].cloneNode(); + } else if (saveClones.test(nodeName)) { + node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode(); + } else { + node = data.createElem(nodeName); + } + + // Avoid adding some elements to fragments in IE < 9 because + // * Attributes like `name` or `type` cannot be set/changed once an element + // is inserted into a document/fragment + // * Link elements with `src` attributes that are inaccessible, as with + // a 403 response, will cause the tab/window to crash + // * Script elements appended to fragments will execute when their `src` + // or `text` property is set + return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node; + } + + /** + * returns a shived DocumentFragment for the given document + * @memberOf html5 + * @param {Document} ownerDocument The context document. + * @returns {Object} The shived DocumentFragment. + */ + function createDocumentFragment(ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createDocumentFragment(); + } + data = data || getExpandoData(ownerDocument); + var clone = data.frag.cloneNode(), + i = 0, + elems = getElements(), + l = elems.length; + for(;i>shiv*/ + + // Assign private properties to the return object with prefix + Modernizr._version = version; + + // expose these for the plugin API. Look in the source for how to join() them against your input + /*>>prefixes*/ + Modernizr._prefixes = prefixes; + /*>>prefixes*/ + /*>>domprefixes*/ + Modernizr._domPrefixes = domPrefixes; + Modernizr._cssomPrefixes = cssomPrefixes; + /*>>domprefixes*/ + + /*>>mq*/ + // Modernizr.mq tests a given media query, live against the current state of the window + // A few important notes: + // * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false + // * A max-width or orientation query will be evaluated against the current state, which may change later. + // * You must specify values. Eg. If you are testing support for the min-width media query use: + // Modernizr.mq('(min-width:0)') + // usage: + // Modernizr.mq('only screen and (max-width:768)') + Modernizr.mq = testMediaQuery; + /*>>mq*/ + + /*>>hasevent*/ + // Modernizr.hasEvent() detects support for a given event, with an optional element to test on + // Modernizr.hasEvent('gesturestart', elem) + Modernizr.hasEvent = isEventSupported; + /*>>hasevent*/ + + /*>>testprop*/ + // Modernizr.testProp() investigates whether a given style property is recognized + // Note that the property names must be provided in the camelCase variant. + // Modernizr.testProp('pointerEvents') + Modernizr.testProp = function(prop){ + return testProps([prop]); + }; + /*>>testprop*/ + + /*>>testallprops*/ + // Modernizr.testAllProps() investigates whether a given style property, + // or any of its vendor-prefixed variants, is recognized + // Note that the property names must be provided in the camelCase variant. + // Modernizr.testAllProps('boxSizing') + Modernizr.testAllProps = testPropsAll; + /*>>testallprops*/ + + + /*>>teststyles*/ + // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards + // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... }) + Modernizr.testStyles = injectElementWithStyles; + /*>>teststyles*/ + + + /*>>prefixed*/ + // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input + // Modernizr.prefixed('boxSizing') // 'MozBoxSizing' + + // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style. + // Return values will also be the camelCase variant, if you need to translate that to hypenated style use: + // + // str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-'); + + // If you're trying to ascertain which transition end event to bind to, you might do something like... + // + // var transEndEventNames = { + // 'WebkitTransition' : 'webkitTransitionEnd', + // 'MozTransition' : 'transitionend', + // 'OTransition' : 'oTransitionEnd', + // 'msTransition' : 'MSTransitionEnd', + // 'transition' : 'transitionend' + // }, + // transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ]; + + Modernizr.prefixed = function(prop, obj, elem){ + if(!obj) { + return testPropsAll(prop, 'pfx'); + } else { + // Testing DOM property e.g. Modernizr.prefixed('requestAnimationFrame', window) // 'mozRequestAnimationFrame' + return testPropsAll(prop, obj, elem); + } + }; + /*>>prefixed*/ + + + /*>>cssclasses*/ + // Remove "no-js" class from element, if it exists: + docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') + + + // Add the new classes to the element. + (enableClasses ? ' js ' + classes.join(' ') : ''); + /*>>cssclasses*/ + + return Modernizr; + +})(this, this.document); diff --git a/examples/module.js b/examples/module.js new file mode 100644 index 0000000..c3826e9 --- /dev/null +++ b/examples/module.js @@ -0,0 +1,5 @@ +"use strict"; +var universe = require('./universe'); +universe.start(); +console.log('The answer is ' + universe.answer); +phantom.exit(); diff --git a/examples/netlog.js b/examples/netlog.js new file mode 100644 index 0000000..0418c23 --- /dev/null +++ b/examples/netlog.js @@ -0,0 +1,26 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'), + address; + +if (system.args.length === 1) { + console.log('Usage: netlog.js '); + phantom.exit(1); +} else { + address = system.args[1]; + + page.onResourceRequested = function (req) { + console.log('requested: ' + JSON.stringify(req, undefined, 4)); + }; + + page.onResourceReceived = function (res) { + console.log('received: ' + JSON.stringify(res, undefined, 4)); + }; + + page.open(address, function (status) { + if (status !== 'success') { + console.log('FAIL to load the address'); + } + phantom.exit(); + }); +} diff --git a/examples/netsniff.js b/examples/netsniff.js new file mode 100644 index 0000000..4655a36 --- /dev/null +++ b/examples/netsniff.js @@ -0,0 +1,144 @@ +"use strict"; +if (!Date.prototype.toISOString) { + Date.prototype.toISOString = function () { + function pad(n) { return n < 10 ? '0' + n : n; } + function ms(n) { return n < 10 ? '00'+ n : n < 100 ? '0' + n : n } + return this.getFullYear() + '-' + + pad(this.getMonth() + 1) + '-' + + pad(this.getDate()) + 'T' + + pad(this.getHours()) + ':' + + pad(this.getMinutes()) + ':' + + pad(this.getSeconds()) + '.' + + ms(this.getMilliseconds()) + 'Z'; + } +} + +function createHAR(address, title, startTime, resources) +{ + var entries = []; + + resources.forEach(function (resource) { + var request = resource.request, + startReply = resource.startReply, + endReply = resource.endReply; + + if (!request || !startReply || !endReply) { + return; + } + + // Exclude Data URI from HAR file because + // they aren't included in specification + if (request.url.match(/(^data:image\/.*)/i)) { + return; + } + + entries.push({ + startedDateTime: request.time.toISOString(), + time: endReply.time - request.time, + request: { + method: request.method, + url: request.url, + httpVersion: "HTTP/1.1", + cookies: [], + headers: request.headers, + queryString: [], + headersSize: -1, + bodySize: -1 + }, + response: { + status: endReply.status, + statusText: endReply.statusText, + httpVersion: "HTTP/1.1", + cookies: [], + headers: endReply.headers, + redirectURL: "", + headersSize: -1, + bodySize: startReply.bodySize, + content: { + size: startReply.bodySize, + mimeType: endReply.contentType + } + }, + cache: {}, + timings: { + blocked: 0, + dns: -1, + connect: -1, + send: 0, + wait: startReply.time - request.time, + receive: endReply.time - startReply.time, + ssl: -1 + }, + pageref: address + }); + }); + + return { + log: { + version: '1.2', + creator: { + name: "PhantomJS", + version: phantom.version.major + '.' + phantom.version.minor + + '.' + phantom.version.patch + }, + pages: [{ + startedDateTime: startTime.toISOString(), + id: address, + title: title, + pageTimings: { + onLoad: page.endTime - page.startTime + } + }], + entries: entries + } + }; +} + +var page = require('webpage').create(), + system = require('system'); + +if (system.args.length === 1) { + console.log('Usage: netsniff.js '); + phantom.exit(1); +} else { + + page.address = system.args[1]; + page.resources = []; + + page.onLoadStarted = function () { + page.startTime = new Date(); + }; + + page.onResourceRequested = function (req) { + page.resources[req.id] = { + request: req, + startReply: null, + endReply: null + }; + }; + + page.onResourceReceived = function (res) { + if (res.stage === 'start') { + page.resources[res.id].startReply = res; + } + if (res.stage === 'end') { + page.resources[res.id].endReply = res; + } + }; + + page.open(page.address, function (status) { + var har; + if (status !== 'success') { + console.log('FAIL to load the address'); + phantom.exit(1); + } else { + page.endTime = new Date(); + page.title = page.evaluate(function () { + return document.title; + }); + har = createHAR(page.address, page.title, page.startTime, page.resources); + console.log(JSON.stringify(har, undefined, 4)); + phantom.exit(); + } + }); +} diff --git a/examples/openurlwithproxy.js b/examples/openurlwithproxy.js new file mode 100644 index 0000000..30b6ffd --- /dev/null +++ b/examples/openurlwithproxy.js @@ -0,0 +1,25 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'), + host, port, address; + +if (system.args.length < 4) { + console.log('Usage: openurlwithproxy.js '); + phantom.exit(1); +} else { + host = system.args[1]; + port = system.args[2]; + address = system.args[3]; + phantom.setProxy(host, port, 'manual', '', ''); + page.open(address, function (status) { + if (status !== 'success') { + console.log('FAIL to load the address "' + + address + '" using proxy "' + host + ':' + port + '"'); + } else { + console.log('Page title is ' + page.evaluate(function () { + return document.title; + })); + } + phantom.exit(); + }); +} diff --git a/examples/outputEncoding.js b/examples/outputEncoding.js new file mode 100644 index 0000000..82af244 --- /dev/null +++ b/examples/outputEncoding.js @@ -0,0 +1,17 @@ +"use strict"; +function helloWorld() { + console.log(phantom.outputEncoding + ": こんにちは、世界!"); +} + +console.log("Using default encoding..."); +helloWorld(); + +console.log("\nUsing other encodings..."); + +var encodings = ["euc-jp", "sjis", "utf8", "System"]; +for (var i = 0; i < encodings.length; i++) { + phantom.outputEncoding = encodings[i]; + helloWorld(); +} + +phantom.exit() diff --git a/examples/page_events.js b/examples/page_events.js new file mode 100644 index 0000000..ef6563e --- /dev/null +++ b/examples/page_events.js @@ -0,0 +1,147 @@ +// The purpose of this is to show how and when events fire, considering 5 steps +// happening as follows: +// +// 1. Load URL +// 2. Load same URL, but adding an internal FRAGMENT to it +// 3. Click on an internal Link, that points to another internal FRAGMENT +// 4. Click on an external Link, that will send the page somewhere else +// 5. Close page +// +// Take particular care when going through the output, to understand when +// things happen (and in which order). Particularly, notice what DOESN'T +// happen during step 3. +// +// If invoked with "-v" it will print out the Page Resources as they are +// Requested and Received. +// +// NOTE.1: The "onConsoleMessage/onAlert/onPrompt/onConfirm" events are +// registered but not used here. This is left for you to have fun with. +// NOTE.2: This script is not here to teach you ANY JavaScript. It's aweful! +// NOTE.3: Main audience for this are people new to PhantomJS. + +"use strict"; +var sys = require("system"), + page = require("webpage").create(), + logResources = false, + step1url = "http://en.wikipedia.org/wiki/DOM_events", + step2url = "http://en.wikipedia.org/wiki/DOM_events#Event_flow"; + +if (sys.args.length > 1 && sys.args[1] === "-v") { + logResources = true; +} + +function printArgs() { + var i, ilen; + for (i = 0, ilen = arguments.length; i < ilen; ++i) { + console.log(" arguments[" + i + "] = " + JSON.stringify(arguments[i])); + } + console.log(""); +} + +//////////////////////////////////////////////////////////////////////////////// + +page.onInitialized = function() { + console.log("page.onInitialized"); + printArgs.apply(this, arguments); +}; +page.onLoadStarted = function() { + console.log("page.onLoadStarted"); + printArgs.apply(this, arguments); +}; +page.onLoadFinished = function() { + console.log("page.onLoadFinished"); + printArgs.apply(this, arguments); +}; +page.onUrlChanged = function() { + console.log("page.onUrlChanged"); + printArgs.apply(this, arguments); +}; +page.onNavigationRequested = function() { + console.log("page.onNavigationRequested"); + printArgs.apply(this, arguments); +}; +page.onRepaintRequested = function() { + console.log("page.onRepaintRequested"); + printArgs.apply(this, arguments); +}; + +if (logResources === true) { + page.onResourceRequested = function() { + console.log("page.onResourceRequested"); + printArgs.apply(this, arguments); + }; + page.onResourceReceived = function() { + console.log("page.onResourceReceived"); + printArgs.apply(this, arguments); + }; +} + +page.onClosing = function() { + console.log("page.onClosing"); + printArgs.apply(this, arguments); +}; + +// window.console.log(msg); +page.onConsoleMessage = function() { + console.log("page.onConsoleMessage"); + printArgs.apply(this, arguments); +}; + +// window.alert(msg); +page.onAlert = function() { + console.log("page.onAlert"); + printArgs.apply(this, arguments); +}; +// var confirmed = window.confirm(msg); +page.onConfirm = function() { + console.log("page.onConfirm"); + printArgs.apply(this, arguments); +}; +// var user_value = window.prompt(msg, default_value); +page.onPrompt = function() { + console.log("page.onPrompt"); + printArgs.apply(this, arguments); +}; + +//////////////////////////////////////////////////////////////////////////////// + +setTimeout(function() { + console.log(""); + console.log("### STEP 1: Load '" + step1url + "'"); + page.open(step1url); +}, 0); + +setTimeout(function() { + console.log(""); + console.log("### STEP 2: Load '" + step2url + "' (load same URL plus FRAGMENT)"); + page.open(step2url); +}, 5000); + +setTimeout(function() { + console.log(""); + console.log("### STEP 3: Click on page internal link (aka FRAGMENT)"); + page.evaluate(function() { + var ev = document.createEvent("MouseEvents"); + ev.initEvent("click", true, true); + document.querySelector("a[href='#Event_object']").dispatchEvent(ev); + }); +}, 10000); + +setTimeout(function() { + console.log(""); + console.log("### STEP 4: Click on page external link"); + page.evaluate(function() { + var ev = document.createEvent("MouseEvents"); + ev.initEvent("click", true, true); + document.querySelector("a[title='JavaScript']").dispatchEvent(ev); + }); +}, 15000); + +setTimeout(function() { + console.log(""); + console.log("### STEP 5: Close page and shutdown (with a delay)"); + page.close(); + setTimeout(function(){ + phantom.exit(); + }, 100); +}, 20000); diff --git a/examples/pagecallback.js b/examples/pagecallback.js new file mode 100644 index 0000000..a14b57e --- /dev/null +++ b/examples/pagecallback.js @@ -0,0 +1,18 @@ +"use strict"; +var p = require("webpage").create(); + +p.onConsoleMessage = function(msg) { console.log(msg); }; + +// Calls to "callPhantom" within the page 'p' arrive here +p.onCallback = function(msg) { + console.log("Received by the 'phantom' main context: "+msg); + return "Hello there, I'm coming to you from the 'phantom' context instead"; +}; + +p.evaluate(function() { + // Return-value of the "onCallback" handler arrive here + var callbackResponse = window.callPhantom("Hello, I'm coming to you from the 'page' context"); + console.log("Received by the 'page' context: "+callbackResponse); +}); + +phantom.exit(); diff --git a/examples/phantomwebintro.js b/examples/phantomwebintro.js new file mode 100644 index 0000000..6cb4e46 --- /dev/null +++ b/examples/phantomwebintro.js @@ -0,0 +1,21 @@ +// Read the Phantom webpage '#intro' element text using jQuery and "includeJs" + +"use strict"; +var page = require('webpage').create(); + +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open("http://phantomjs.org/", function(status) { + if (status === "success") { + page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function() { + page.evaluate(function() { + console.log("$(\".explanation\").text() -> " + $(".explanation").text()); + }); + phantom.exit(0); + }); + } else { + phantom.exit(1); + } +}); diff --git a/examples/post.js b/examples/post.js new file mode 100644 index 0000000..4b998b8 --- /dev/null +++ b/examples/post.js @@ -0,0 +1,15 @@ +// Example using HTTP POST operation + +"use strict"; +var page = require('webpage').create(), + server = 'http://posttestserver.com/post.php?dump', + data = 'universe=expanding&answer=42'; + +page.open(server, 'post', data, function (status) { + if (status !== 'success') { + console.log('Unable to post!'); + } else { + console.log(page.content); + } + phantom.exit(); +}); diff --git a/examples/postjson.js b/examples/postjson.js new file mode 100644 index 0000000..b02f430 --- /dev/null +++ b/examples/postjson.js @@ -0,0 +1,19 @@ +// Example using HTTP POST operation + +"use strict"; +var page = require('webpage').create(), + server = 'http://posttestserver.com/post.php?dump', + data = '{"universe": "expanding", "answer": 42}'; + +var headers = { + "Content-Type": "application/json" +} + +page.open(server, 'post', data, headers, function (status) { + if (status !== 'success') { + console.log('Unable to post!'); + } else { + console.log(page.content); + } + phantom.exit(); +}); diff --git a/examples/postserver.js b/examples/postserver.js new file mode 100644 index 0000000..e854701 --- /dev/null +++ b/examples/postserver.js @@ -0,0 +1,35 @@ +// Example using HTTP POST operation + +"use strict"; +var page = require('webpage').create(), + server = require('webserver').create(), + system = require('system'), + data = 'universe=expanding&answer=42'; + +if (system.args.length !== 2) { + console.log('Usage: postserver.js '); + phantom.exit(1); +} + +var port = system.args[1]; + +service = server.listen(port, function (request, response) { + console.log('Request received at ' + new Date()); + + response.statusCode = 200; + response.headers = { + 'Cache': 'no-cache', + 'Content-Type': 'text/plain;charset=utf-8' + }; + response.write(JSON.stringify(request, null, 4)); + response.close(); +}); + +page.open('http://localhost:' + port + '/', 'post', data, function (status) { + if (status !== 'success') { + console.log('Unable to post!'); + } else { + console.log(page.plainText); + } + phantom.exit(); +}); diff --git a/examples/printenv.js b/examples/printenv.js new file mode 100644 index 0000000..6baea03 --- /dev/null +++ b/examples/printenv.js @@ -0,0 +1,10 @@ +var system = require('system'), + env = system.env, + key; + +for (key in env) { + if (env.hasOwnProperty(key)) { + console.log(key + '=' + env[key]); + } +} +phantom.exit(); diff --git a/examples/printheaderfooter.js b/examples/printheaderfooter.js new file mode 100644 index 0000000..286af2b --- /dev/null +++ b/examples/printheaderfooter.js @@ -0,0 +1,90 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'); + +function someCallback(pageNum, numPages) { + return "

someCallback: " + pageNum + " / " + numPages + "

"; +} + +if (system.args.length < 3) { + console.log('Usage: printheaderfooter.js URL filename'); + phantom.exit(1); +} else { + var address = system.args[1]; + var output = system.args[2]; + page.viewportSize = { width: 600, height: 600 }; + page.paperSize = { + format: 'A4', + margin: "1cm", + /* default header/footer for pages that don't have custom overwrites (see below) */ + header: { + height: "1cm", + contents: phantom.callback(function(pageNum, numPages) { + if (pageNum == 1) { + return ""; + } + return "

Header " + pageNum + " / " + numPages + "

"; + }) + }, + footer: { + height: "1cm", + contents: phantom.callback(function(pageNum, numPages) { + if (pageNum == numPages) { + return ""; + } + return "

Footer " + pageNum + " / " + numPages + "

"; + }) + } + }; + page.open(address, function (status) { + if (status !== 'success') { + console.log('Unable to load the address!'); + } else { + /* check whether the loaded page overwrites the header/footer setting, + i.e. whether a PhantomJSPriting object exists. Use that then instead + of our defaults above. + + example: + + + + +

asdfadsf

asdfadsfycvx

+ + */ + if (page.evaluate(function(){return typeof PhantomJSPrinting == "object";})) { + paperSize = page.paperSize; + paperSize.header.height = page.evaluate(function() { + return PhantomJSPrinting.header.height; + }); + paperSize.header.contents = phantom.callback(function(pageNum, numPages) { + return page.evaluate(function(pageNum, numPages){return PhantomJSPrinting.header.contents(pageNum, numPages);}, pageNum, numPages); + }); + paperSize.footer.height = page.evaluate(function() { + return PhantomJSPrinting.footer.height; + }); + paperSize.footer.contents = phantom.callback(function(pageNum, numPages) { + return page.evaluate(function(pageNum, numPages){return PhantomJSPrinting.footer.contents(pageNum, numPages);}, pageNum, numPages); + }); + page.paperSize = paperSize; + console.log(page.paperSize.header.height); + console.log(page.paperSize.footer.height); + } + window.setTimeout(function () { + page.render(output); + phantom.exit(); + }, 200); + } + }); +} diff --git a/examples/printmargins.js b/examples/printmargins.js new file mode 100644 index 0000000..57c1acc --- /dev/null +++ b/examples/printmargins.js @@ -0,0 +1,36 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'); + +if (system.args.length < 7) { + console.log('Usage: printmargins.js URL filename LEFT TOP RIGHT BOTTOM'); + console.log(' margin examples: "1cm", "10px", "7mm", "5in"'); + phantom.exit(1); +} else { + var address = system.args[1]; + var output = system.args[2]; + var marginLeft = system.args[3]; + var marginTop = system.args[4]; + var marginRight = system.args[5]; + var marginBottom = system.args[6]; + page.viewportSize = { width: 600, height: 600 }; + page.paperSize = { + format: 'A4', + margin: { + left: marginLeft, + top: marginTop, + right: marginRight, + bottom: marginBottom + } + }; + page.open(address, function (status) { + if (status !== 'success') { + console.log('Unable to load the address!'); + } else { + window.setTimeout(function () { + page.render(output); + phantom.exit(); + }, 200); + } + }); +} diff --git a/examples/rasterize.js b/examples/rasterize.js new file mode 100644 index 0000000..c0950de --- /dev/null +++ b/examples/rasterize.js @@ -0,0 +1,49 @@ +"use strict"; +var page = require('webpage').create(), + system = require('system'), + address, output, size; + +if (system.args.length < 3 || system.args.length > 5) { + console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]'); + console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"'); + console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px'); + console.log(' "800px*600px" window, clipped to 800x600'); + phantom.exit(1); +} else { + address = system.args[1]; + output = system.args[2]; + page.viewportSize = { width: 600, height: 600 }; + if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") { + size = system.args[3].split('*'); + page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' } + : { format: system.args[3], orientation: 'portrait', margin: '1cm' }; + } else if (system.args.length > 3 && system.args[3].substr(-2) === "px") { + size = system.args[3].split('*'); + if (size.length === 2) { + pageWidth = parseInt(size[0], 10); + pageHeight = parseInt(size[1], 10); + page.viewportSize = { width: pageWidth, height: pageHeight }; + page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight }; + } else { + console.log("size:", system.args[3]); + pageWidth = parseInt(system.args[3], 10); + pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any + console.log ("pageHeight:",pageHeight); + page.viewportSize = { width: pageWidth, height: pageHeight }; + } + } + if (system.args.length > 4) { + page.zoomFactor = system.args[4]; + } + page.open(address, function (status) { + if (status !== 'success') { + console.log('Unable to load the address!'); + phantom.exit(1); + } else { + window.setTimeout(function () { + page.render(output); + phantom.exit(); + }, 200); + } + }); +} diff --git a/examples/render_multi_url.js b/examples/render_multi_url.js new file mode 100644 index 0000000..9f7348d --- /dev/null +++ b/examples/render_multi_url.js @@ -0,0 +1,74 @@ +// Render Multiple URLs to file + +"use strict"; +var RenderUrlsToFile, arrayOfUrls, system; + +system = require("system"); + +/* +Render given urls +@param array of URLs to render +@param callbackPerUrl Function called after finishing each URL, including the last URL +@param callbackFinal Function called after finishing everything +*/ +RenderUrlsToFile = function(urls, callbackPerUrl, callbackFinal) { + var getFilename, next, page, retrieve, urlIndex, webpage; + urlIndex = 0; + webpage = require("webpage"); + page = null; + getFilename = function() { + return "rendermulti-" + urlIndex + ".png"; + }; + next = function(status, url, file) { + page.close(); + callbackPerUrl(status, url, file); + return retrieve(); + }; + retrieve = function() { + var url; + if (urls.length > 0) { + url = urls.shift(); + urlIndex++; + page = webpage.create(); + page.viewportSize = { + width: 800, + height: 600 + }; + page.settings.userAgent = "Phantom.js bot"; + return page.open("http://" + url, function(status) { + var file; + file = getFilename(); + if (status === "success") { + return window.setTimeout((function() { + page.render(file); + return next(status, url, file); + }), 200); + } else { + return next(status, url, file); + } + }); + } else { + return callbackFinal(); + } + }; + return retrieve(); +}; + +arrayOfUrls = null; + +if (system.args.length > 1) { + arrayOfUrls = Array.prototype.slice.call(system.args, 1); +} else { + console.log("Usage: phantomjs render_multi_url.js [domain.name1, domain.name2, ...]"); + arrayOfUrls = ["www.google.com", "www.bbc.co.uk", "phantomjs.org"]; +} + +RenderUrlsToFile(arrayOfUrls, (function(status, url, file) { + if (status !== "success") { + return console.log("Unable to render '" + url + "'"); + } else { + return console.log("Rendered '" + url + "' at '" + file + "'"); + } +}), function() { + return phantom.exit(); +}); diff --git a/examples/responsive-screenshot.js b/examples/responsive-screenshot.js new file mode 100644 index 0000000..35aac60 --- /dev/null +++ b/examples/responsive-screenshot.js @@ -0,0 +1,181 @@ +/** + * Captures the full height document even if it's not showing on the screen or captures with the provided range of screen sizes. + * + * A basic example for taking a screen shot using phantomjs which is sampled for https://nodejs-dersleri.github.io/ + * + * usage : phantomjs responsive-screenshot.js {url} [output format] [doClipping] + * + * examples > + * phantomjs responsive-screenshot.js https://nodejs-dersleri.github.io/ + * phantomjs responsive-screenshot.js https://nodejs-dersleri.github.io/ pdf + * phantomjs responsive-screenshot.js https://nodejs-dersleri.github.io/ true + * phantomjs responsive-screenshot.js https://nodejs-dersleri.github.io/ png true + * + * @author Salih sagdilek + */ + +/** + * http://phantomjs.org/api/system/property/args.html + * + * Queries and returns a list of the command-line arguments. + * The first one is always the script name, which is then followed by the subsequent arguments. + */ +var args = require('system').args; +/** + * http://phantomjs.org/api/fs/ + * + * file system api + */ +var fs = require('fs'); + +/** + * http://phantomjs.org/api/webpage/ + * + * Web page api + */ +var page = new WebPage(); + +/** + * if url address does not exist, exit phantom + */ +if ( 1 === args.length ) { + console.log('Url address is required'); + phantom.exit(); +} + +/** + * setup url address (second argument); + */ +var urlAddress = args[1].toLowerCase(); + + +/** + * set output extension format + * @type {*} + */ +var ext = getFileExtension(); + +/** + * set if clipping ? + * @type {boolean} + */ +var clipping = getClipping(); + +/** + * setup viewports + */ +var viewports = [ + { + width : 1200, + height : 800 + }, + { + width : 1024, + height : 768 + }, + { + width : 768, + height : 1024 + }, + { + width : 480, + height : 640 + }, + { + width : 320, + height : 480 + } +]; + +page.open(urlAddress, function (status) { + if ( 'success' !== status ) { + console.log('Unable to load the url address!'); + } else { + var folder = urlToDir(urlAddress); + var output, key; + + function render(n) { + if ( !!n ) { + key = n - 1; + page.viewportSize = viewports[key]; + if ( clipping ) { + page.clipRect = viewports[key]; + } + output = folder + "/" + getFileName(viewports[key]); + console.log('Saving ' + output); + page.render(output); + render(key); + } + } + + render(viewports.length); + } + phantom.exit(); +}); + +/** + * filename generator helper + * @param viewport + * @returns {string} + */ +function getFileName(viewport) { + var d = new Date(); + var date = [ + d.getUTCFullYear(), + d.getUTCMonth() + 1, + d.getUTCDate() + ]; + var time = [ + d.getHours() <= 9 ? '0' + d.getHours() : d.getHours(), + d.getMinutes() <= 9 ? '0' + d.getMinutes() : d.getMinutes(), + d.getSeconds() <= 9 ? '0' + d.getSeconds() : d.getSeconds(), + d.getMilliseconds() + ]; + var resolution = viewport.width + (clipping ? "x" + viewport.height : ''); + + return date.join('-') + '_' + time.join('-') + "_" + resolution + ext; +} + +/** + * output extension format helper + * + * @returns {*} + */ +function getFileExtension() { + if ( 'true' != args[2] && !!args[2] ) { + return '.' + args[2]; + } + return '.png'; +} + +/** + * check if clipping + * + * @returns {boolean} + */ +function getClipping() { + if ( 'true' == args[3] ) { + return !!args[3]; + } else if ( 'true' == args[2] ) { + return !!args[2]; + } + return false; +} + +/** + * url to directory helper + * + * @param url + * @returns {string} + */ +function urlToDir(url) { + var dir = url + .replace(/^(http|https):\/\//, '') + .replace(/\/$/, ''); + + if ( !fs.makeTree(dir) ) { + console.log('"' + dir + '" is NOT created.'); + phantom.exit(); + } + return dir; +} diff --git a/examples/run-jasmine.js b/examples/run-jasmine.js new file mode 100644 index 0000000..652bb61 --- /dev/null +++ b/examples/run-jasmine.js @@ -0,0 +1,92 @@ +"use strict"; +var system = require('system'); + +/** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param testFx javascript condition that evaluates to a boolean, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param onReady what to do when testFx condition is fulfilled, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. + */ +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s + start = new Date().getTime(), + condition = false, + interval = setInterval(function() { + if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code + } else { + if(!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("'waitFor()' timeout"); + phantom.exit(1); + } else { + // Condition fulfilled (timeout and/or condition is 'true') + console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); + typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval); //< Stop this interval + } + } + }, 100); //< repeat check every 100ms +}; + + +if (system.args.length !== 2) { + console.log('Usage: run-jasmine.js URL'); + phantom.exit(1); +} + +var page = require('webpage').create(); + +// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open(system.args[1], function(status){ + if (status !== "success") { + console.log("Unable to open " + system.args[1]); + phantom.exit(1); + } else { + waitFor(function(){ + return page.evaluate(function(){ + return document.body.querySelector('.symbolSummary .pending') === null + }); + }, function(){ + var exitCode = page.evaluate(function(){ + try { + console.log(''); + console.log(document.body.querySelector('.description').innerText); + var list = document.body.querySelectorAll('.results > #details > .specDetail.failed'); + if (list && list.length > 0) { + console.log(''); + console.log(list.length + ' test(s) FAILED:'); + for (i = 0; i < list.length; ++i) { + var el = list[i], + desc = el.querySelector('.description'), + msg = el.querySelector('.resultMessage.fail'); + console.log(''); + console.log(desc.innerText); + console.log(msg.innerText); + console.log(''); + } + return 1; + } else { + console.log(document.body.querySelector('.alert > .passingAlert.bar').innerText); + return 0; + } + } catch (ex) { + console.log(ex); + return 1; + } + }); + phantom.exit(exitCode); + }); + } +}); diff --git a/examples/run-jasmine2.js b/examples/run-jasmine2.js new file mode 100644 index 0000000..343117a --- /dev/null +++ b/examples/run-jasmine2.js @@ -0,0 +1,94 @@ +"use strict"; +var system = require('system'); + +/** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param testFx javascript condition that evaluates to a boolean, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param onReady what to do when testFx condition is fulfilled, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. + */ +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s + start = new Date().getTime(), + condition = false, + interval = setInterval(function() { + if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code + } else { + if(!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("'waitFor()' timeout"); + phantom.exit(1); + } else { + // Condition fulfilled (timeout and/or condition is 'true') + console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); + typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval); //< Stop this interval + } + } + }, 100); //< repeat check every 100ms +}; + + +if (system.args.length !== 2) { + console.log('Usage: run-jasmine2.js URL'); + phantom.exit(1); +} + +var page = require('webpage').create(); + +// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open(system.args[1], function(status){ + if (status !== "success") { + console.log("Unable to access network"); + phantom.exit(); + } else { + waitFor(function(){ + return page.evaluate(function(){ + return (document.body.querySelector('.symbolSummary .pending') === null && + document.body.querySelector('.duration') !== null); + }); + }, function(){ + var exitCode = page.evaluate(function(){ + console.log(''); + + var title = 'Jasmine'; + var version = document.body.querySelector('.version').innerText; + var duration = document.body.querySelector('.duration').innerText; + var banner = title + ' ' + version + ' ' + duration; + console.log(banner); + + var list = document.body.querySelectorAll('.results > .failures > .spec-detail.failed'); + if (list && list.length > 0) { + console.log(''); + console.log(list.length + ' test(s) FAILED:'); + for (i = 0; i < list.length; ++i) { + var el = list[i], + desc = el.querySelector('.description'), + msg = el.querySelector('.messages > .result-message'); + console.log(''); + console.log(desc.innerText); + console.log(msg.innerText); + console.log(''); + } + return 1; + } else { + console.log(document.body.querySelector('.alert > .bar.passed,.alert > .bar.skipped').innerText); + return 0; + } + }); + phantom.exit(exitCode); + }); + } +}); diff --git a/examples/run-qunit.js b/examples/run-qunit.js new file mode 100644 index 0000000..d3cbf5b --- /dev/null +++ b/examples/run-qunit.js @@ -0,0 +1,77 @@ +"use strict"; +var system = require('system'); + +/** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param testFx javascript condition that evaluates to a boolean, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param onReady what to do when testFx condition is fulfilled, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. + */ +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s + start = new Date().getTime(), + condition = false, + interval = setInterval(function() { + if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code + } else { + if(!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("'waitFor()' timeout"); + phantom.exit(1); + } else { + // Condition fulfilled (timeout and/or condition is 'true') + console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); + typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval); //< Stop this interval + } + } + }, 100); //< repeat check every 250ms +}; + + +if (system.args.length !== 2) { + console.log('Usage: run-qunit.js URL'); + phantom.exit(1); +} + +var page = require('webpage').create(); + +// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.open(system.args[1], function(status){ + if (status !== "success") { + console.log("Unable to access network"); + phantom.exit(1); + } else { + waitFor(function(){ + return page.evaluate(function(){ + var el = document.getElementById('qunit-testresult'); + if (el && el.innerText.match('completed')) { + return true; + } + return false; + }); + }, function(){ + var failedNum = page.evaluate(function(){ + var el = document.getElementById('qunit-testresult'); + console.log(el.innerText); + try { + return el.getElementsByClassName('failed')[0].innerHTML; + } catch (e) { } + return 10000; + }); + phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0); + }); + } +}); diff --git a/examples/scandir.js b/examples/scandir.js new file mode 100644 index 0000000..7394367 --- /dev/null +++ b/examples/scandir.js @@ -0,0 +1,24 @@ +// List all the files in a Tree of Directories + +"use strict"; +var system = require('system'); + +if (system.args.length !== 2) { + console.log("Usage: phantomjs scandir.js DIRECTORY_TO_SCAN"); + phantom.exit(1); +} + +var scanDirectory = function (path) { + var fs = require('fs'); + if (fs.exists(path) && fs.isFile(path)) { + console.log(path); + } else if (fs.isDirectory(path)) { + fs.list(path).forEach(function (e) { + if ( e !== "." && e !== ".." ) { //< Avoid loops + scanDirectory(path + '/' + e); + } + }); + } +}; +scanDirectory(system.args[1]); +phantom.exit(); diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..ff5ef3d --- /dev/null +++ b/examples/server.js @@ -0,0 +1,44 @@ +"use strict"; +var page = require('webpage').create(); +var server = require('webserver').create(); +var system = require('system'); +var host, port; + +if (system.args.length !== 2) { + console.log('Usage: server.js '); + phantom.exit(1); +} else { + port = system.args[1]; + var listening = server.listen(port, function (request, response) { + console.log("GOT HTTP REQUEST"); + console.log(JSON.stringify(request, null, 4)); + + // we set the headers here + response.statusCode = 200; + response.headers = {"Cache": "no-cache", "Content-Type": "text/html"}; + // this is also possible: + response.setHeader("foo", "bar"); + // now we write the body + // note: the headers above will now be sent implictly + response.write("YES!"); + // note: writeBody can be called multiple times + response.write("

pretty cool :)"); + response.close(); + }); + if (!listening) { + console.log("could not create web server listening on port " + port); + phantom.exit(); + } + var url = "http://localhost:" + port + "/foo/bar.php?asdf=true"; + console.log("SENDING REQUEST TO:"); + console.log(url); + page.open(url, function (status) { + if (status !== 'success') { + console.log('FAIL to load the address'); + } else { + console.log("GOT REPLY FROM SERVER:"); + console.log(page.content); + } + phantom.exit(); + }); +} diff --git a/examples/serverkeepalive.js b/examples/serverkeepalive.js new file mode 100644 index 0000000..00b462a --- /dev/null +++ b/examples/serverkeepalive.js @@ -0,0 +1,35 @@ +"use strict"; +var port, server, service, + system = require('system'); + +if (system.args.length !== 2) { + console.log('Usage: serverkeepalive.js '); + phantom.exit(1); +} else { + port = system.args[1]; + server = require('webserver').create(); + + service = server.listen(port, { keepAlive: true }, function (request, response) { + console.log('Request at ' + new Date()); + console.log(JSON.stringify(request, null, 4)); + + var body = JSON.stringify(request, null, 4); + response.statusCode = 200; + response.headers = { + 'Cache': 'no-cache', + 'Content-Type': 'text/plain', + 'Connection': 'Keep-Alive', + 'Keep-Alive': 'timeout=5, max=100', + 'Content-Length': body.length + }; + response.write(body); + response.close(); + }); + + if (service) { + console.log('Web server running on port ' + port); + } else { + console.log('Error: Could not create web server listening on port ' + port); + phantom.exit(); + } +} diff --git a/examples/simpleserver.js b/examples/simpleserver.js new file mode 100644 index 0000000..ba4779a --- /dev/null +++ b/examples/simpleserver.js @@ -0,0 +1,43 @@ +"use strict"; +var port, server, service, + system = require('system'); + +if (system.args.length !== 2) { + console.log('Usage: simpleserver.js '); + phantom.exit(1); +} else { + port = system.args[1]; + server = require('webserver').create(); + + service = server.listen(port, function (request, response) { + + console.log('Request at ' + new Date()); + console.log(JSON.stringify(request, null, 4)); + + response.statusCode = 200; + response.headers = { + 'Cache': 'no-cache', + 'Content-Type': 'text/html' + }; + response.write(''); + response.write(''); + response.write('Hello, world!'); + response.write(''); + response.write(''); + response.write('

This is from PhantomJS web server.

'); + response.write('

Request data:

'); + response.write('
');
+        response.write(JSON.stringify(request, null, 4));
+        response.write('
'); + response.write(''); + response.write(''); + response.close(); + }); + + if (service) { + console.log('Web server running on port ' + port); + } else { + console.log('Error: Could not create web server listening on port ' + port); + phantom.exit(); + } +} diff --git a/examples/sleepsort.js b/examples/sleepsort.js new file mode 100644 index 0000000..7959799 --- /dev/null +++ b/examples/sleepsort.js @@ -0,0 +1,27 @@ +// sleepsort.js - Sort integers from the commandline in a very ridiculous way: leveraging timeouts :P + +"use strict"; +var system = require('system'); + +function sleepSort(array, callback) { + var sortedCount = 0, + i, len; + for ( i = 0, len = array.length; i < len; ++i ) { + setTimeout((function(j){ + return function() { + console.log(array[j]); + ++sortedCount; + (len === sortedCount) && callback(); + }; + }(i)), array[i]); + } +} + +if ( system.args.length < 2 ) { + console.log("Usage: phantomjs sleepsort.js PUT YOUR INTEGERS HERE SEPARATED BY SPACES"); + phantom.exit(1); +} else { + sleepSort(system.args.slice(1), function() { + phantom.exit(); + }); +} diff --git a/examples/stdin-stdout-stderr.js b/examples/stdin-stdout-stderr.js new file mode 100644 index 0000000..4161ba3 --- /dev/null +++ b/examples/stdin-stdout-stderr.js @@ -0,0 +1,19 @@ +"use strict"; +var system = require('system'); + +system.stdout.write('Hello, system.stdout.write!'); +system.stdout.writeLine('\nHello, system.stdout.writeLine!'); + +system.stderr.write('Hello, system.stderr.write!'); +system.stderr.writeLine('\nHello, system.stderr.writeLine!'); + +system.stdout.writeLine('system.stdin.readLine(): '); +var line = system.stdin.readLine(); +system.stdout.writeLine(JSON.stringify(line)); + +// This is essentially a `readAll` +system.stdout.writeLine('system.stdin.read(5): (ctrl+D to end)'); +var input = system.stdin.read(5); +system.stdout.writeLine(JSON.stringify(input)); + +phantom.exit(0); diff --git a/examples/universe.js b/examples/universe.js new file mode 100644 index 0000000..2655aaf --- /dev/null +++ b/examples/universe.js @@ -0,0 +1,10 @@ +// This is to be used by "module.js" (and "module.coffee") example(s). +// There should NOT be a "universe.coffee" as only 1 of the 2 would +// ever be loaded unless the file extension was specified. + +"use strict"; +exports.answer = 42; + +exports.start = function () { + console.log('Starting the universe....'); +} diff --git a/examples/unrandomize.js b/examples/unrandomize.js new file mode 100644 index 0000000..7f0e1bb --- /dev/null +++ b/examples/unrandomize.js @@ -0,0 +1,25 @@ +// Modify global object at the page initialization. +// In this example, effectively Math.random() always returns 0.42. + +"use strict"; +var page = require('webpage').create(); + +page.onInitialized = function () { + page.evaluate(function () { + Math.random = function() { + return 42 / 100; + }; + }); +}; + +page.open('http://ariya.github.com/js/random/', function (status) { + var result; + if (status !== 'success') { + console.log('Network error.'); + } else { + console.log(page.evaluate(function () { + return document.getElementById('numbers').textContent; + })); + } + phantom.exit(); +}); diff --git a/examples/useragent.js b/examples/useragent.js new file mode 100644 index 0000000..5a48091 --- /dev/null +++ b/examples/useragent.js @@ -0,0 +1,15 @@ +"use strict"; +var page = require('webpage').create(); +console.log('The default user agent is ' + page.settings.userAgent); +page.settings.userAgent = 'SpecialAgent'; +page.open('http://www.httpuseragent.org', function (status) { + if (status !== 'success') { + console.log('Unable to access network'); + } else { + var ua = page.evaluate(function () { + return document.getElementById('myagent').innerText; + }); + console.log(ua); + } + phantom.exit(); +}); diff --git a/examples/version.js b/examples/version.js new file mode 100644 index 0000000..dfda4f8 --- /dev/null +++ b/examples/version.js @@ -0,0 +1,6 @@ +"use strict"; +console.log('using PhantomJS version ' + + phantom.version.major + '.' + + phantom.version.minor + '.' + + phantom.version.patch); +phantom.exit(); diff --git a/examples/waitfor.js b/examples/waitfor.js new file mode 100644 index 0000000..c470a92 --- /dev/null +++ b/examples/waitfor.js @@ -0,0 +1,58 @@ +/** + * Wait until the test condition is true or a timeout occurs. Useful for waiting + * on a server response or for a ui change (fadeIn, etc.) to occur. + * + * @param testFx javascript condition that evaluates to a boolean, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param onReady what to do when testFx condition is fulfilled, + * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or + * as a callback function. + * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used. + */ + +"use strict"; +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s + start = new Date().getTime(), + condition = false, + interval = setInterval(function() { + if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code + } else { + if(!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("'waitFor()' timeout"); + phantom.exit(1); + } else { + // Condition fulfilled (timeout and/or condition is 'true') + console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms."); + typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval); //< Stop this interval + } + } + }, 250); //< repeat check every 250ms +}; + + +var page = require('webpage').create(); + +// Open Twitter on 'sencha' profile and, onPageLoad, do... +page.open("http://twitter.com/#!/sencha", function (status) { + // Check for page load success + if (status !== "success") { + console.log("Unable to access network"); + } else { + // Wait for 'signin-dropdown' to be visible + waitFor(function() { + // Check in the page if a specific element is now visible + return page.evaluate(function() { + return $("#signin-dropdown").is(":visible"); + }); + }, function() { + console.log("The sign-in dialog should be visible now."); + phantom.exit(); + }); + } +}); diff --git a/examples/walk_through_frames.js b/examples/walk_through_frames.js new file mode 100644 index 0000000..d0fabfc --- /dev/null +++ b/examples/walk_through_frames.js @@ -0,0 +1,73 @@ +"use strict"; +var p = require("webpage").create(); + +function pageTitle(page) { + return page.evaluate(function(){ + return window.document.title; + }); +} + +function setPageTitle(page, newTitle) { + page.evaluate(function(newTitle){ + window.document.title = newTitle; + }, newTitle); +} + +p.open("../test/webpage-spec-frames/index.html", function(status) { + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToChildFrame(\"frame1\"): "+p.switchToChildFrame("frame1")); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToChildFrame(\"frame1-2\"): "+p.switchToChildFrame("frame1-2")); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToParentFrame(): "+p.switchToParentFrame()); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToChildFrame(0): "+p.switchToChildFrame(0)); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToMainFrame()"); p.switchToMainFrame(); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + console.log("p.switchToChildFrame(\"frame2\"): "+p.switchToChildFrame("frame2")); + console.log("pageTitle(): " + pageTitle(p)); + console.log("currentFrameName(): "+p.currentFrameName()); + console.log("childFramesCount(): "+p.childFramesCount()); + console.log("childFramesName(): "+p.childFramesName()); + console.log("setPageTitle(CURRENT TITLE+'-visited')"); setPageTitle(p, pageTitle(p) + "-visited"); + console.log(""); + + phantom.exit(); +}); diff --git a/phantomjs.pro b/phantomjs.pro new file mode 100644 index 0000000..24663df --- /dev/null +++ b/phantomjs.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs +CONFIG += ordered +SUBDIRS += src/phantomjs.pro diff --git a/src/.gitignore-breakpad b/src/.gitignore-breakpad new file mode 100644 index 0000000..61bec96 --- /dev/null +++ b/src/.gitignore-breakpad @@ -0,0 +1,18 @@ +/src/client/linux/linux_dumper_unittest_helper +/src/processor/minidump_dump +/src/processor/minidump_stackwalk +/src/tools/linux/core2md/core2md +/src/tools/linux/dump_syms/dump_syms +/src/tools/linux/md2core/minidump-2-core +/src/tools/linux/symupload/minidump_upload +/src/tools/linux/symupload/sym_upload +/src/config.h +/src/stamp-h1 +/config.log +/config.status +/autom4te.cache +!Makefile.am +!Makefile.in + +.dirstamp +.deps diff --git a/src/Info.plist b/src/Info.plist new file mode 100644 index 0000000..2ee6234 --- /dev/null +++ b/src/Info.plist @@ -0,0 +1,12 @@ + + + + +CFBundleExecutable +phantomjs +CFBundleIdentifier +org.phantomjs +LSUIElement +1 + + diff --git a/src/bootstrap.js b/src/bootstrap.js new file mode 100644 index 0000000..788d452 --- /dev/null +++ b/src/bootstrap.js @@ -0,0 +1,327 @@ +/*jslint sloppy: true, nomen: true */ +/*global window:true,phantom:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 Ivan De Marino + Copyright (C) 2011 James Roe + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + Copyright (C) 2012 James M. Greene + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +phantom.__defineErrorSignalHandler__ = function(obj, page, handlers) { + var handlerName = 'onError'; + + Object.defineProperty(obj, handlerName, { + set: function (f) { + // Disconnect previous handler (if any) + var handlerObj = handlers[handlerName]; + if (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") { + try { page.javaScriptErrorSent.disconnect(handlerObj.connector); } + catch (e) { } + } + + // Delete the previous handler + delete handlers[handlerName]; + + if (typeof f === 'function') { + var connector = function (message, lineNumber, source, stack) { + var revisedStack = JSON.parse(stack).map(function (item) { + return { file: item.url, line: item.lineNumber, function: item.functionName }; + }); + if (revisedStack.length == 0) + revisedStack = [{ file: source, line: lineNumber }]; + + f(message, revisedStack); + }; + // Store the new handler for reference + handlers[handlerName] = { + callback: f, + connector: connector + }; + + page.javaScriptErrorSent.connect(connector); + } + }, + get: function () { + var handlerObj = handlers[handlerName]; + return (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") ? + handlers[handlerName].callback : + undefined; + } + }); +}; + +(function() { + var handlers = {}; + phantom.__defineErrorSignalHandler__(phantom, phantom.page, handlers); +})(); + +// TODO: Make this output to STDERR +phantom.defaultErrorHandler = function(message, stack) { + console.log(message + "\n"); + + stack.forEach(function(item) { + var message = item.file + ":" + item.line; + if (item["function"]) + message += " in " + item["function"]; + console.log(" " + message); + }); +}; + +phantom.onError = phantom.defaultErrorHandler; + +phantom.callback = function(callback) { + var ret = phantom.createCallback(); + ret.called.connect(function(args) { + var retVal = callback.apply(this, args); + ret.returnValue = retVal; + }); + return ret; +}; + +(function() { + // CommonJS module implementation follows + + window.global = window; + // fs is loaded at the end, when everything is ready + var fs; + var cache = {}; + var paths = []; + // use getters to initialize lazily + // (for future, now both fs and system are loaded anyway) + var nativeExports = { + get fs() { return phantom.createFilesystem(); }, + get child_process() { return phantom._createChildProcess(); }, + get system() { return phantom.createSystem(); } + }; + var extensions = { + '.js': function(module, filename) { + var code = fs.read(filename); + module._compile(code); + }, + + '.json': function(module, filename) { + module.exports = JSON.parse(fs.read(filename)); + } + }; + + function loadFs() { + var file, code, module, filename = ':/modules/fs.js'; + + module = new Module(filename); + cache[filename] = module; + module.exports = nativeExports.fs; + + file = module.exports._open(filename, { mode: 'r' }) + code = file.read(); + file.close(); + module._compile(code); + + return module.exports; + } + + function dirname(path) { + var replaced = path.replace(/\/[^\/]*\/?$/, ''); + if (replaced == path) { + replaced = ''; + } + return replaced; + } + + function basename(path) { + return path.replace(/.*\//, ''); + } + + function joinPath() { + // It should be okay to hard-code a slash here. + // The FileSystem module returns a platform-specific + // separator, but the JavaScript engine only expects + // the slash. + var args = Array.prototype.slice.call(arguments); + return args.join('/'); + } + + function tryFile(path) { + if (fs.isFile(path)) return path; + return null; + } + + function tryExtensions(path) { + var filename, exts = Object.keys(extensions); + for (var i=0; i + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "callback.h" + +#include + +Callback::Callback(QObject* parent) + : QObject(parent) +{ +} + +QVariant Callback::call(const QVariantList& arguments) +{ + emit called(arguments); + + qDebug() << "Callback - call result:" << m_returnValue; + return m_returnValue; +} + +QVariant Callback::returnValue() const +{ + return m_returnValue; +} + +void Callback::setReturnValue(const QVariant& returnValue) +{ + m_returnValue = returnValue; +} diff --git a/src/callback.h b/src/callback.h new file mode 100644 index 0000000..318fb4f --- /dev/null +++ b/src/callback.h @@ -0,0 +1,57 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 Milian Wolff, KDAB + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CALLBACK_H +#define CALLBACK_H + +#include +#include + +class Callback : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QVariant returnValue READ returnValue WRITE setReturnValue) + +public: + Callback(QObject* parent); + + QVariant call(const QVariantList& arguments); + + QVariant returnValue() const; + void setReturnValue(const QVariant& returnValue); + +signals: + void called(const QVariantList& arguments); + +private: + QVariant m_returnValue; +}; + +#endif // CALLBACK_H diff --git a/src/childprocess.cpp b/src/childprocess.cpp new file mode 100644 index 0000000..e3ed3fc --- /dev/null +++ b/src/childprocess.cpp @@ -0,0 +1,136 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "childprocess.h" + +// +// ChildProcessContext +// + +ChildProcessContext::ChildProcessContext(QObject* parent) + : QObject(parent) + , m_proc(this) +{ + connect(&m_proc, SIGNAL(readyReadStandardOutput()), this, SLOT(_readyReadStandardOutput())); + connect(&m_proc, SIGNAL(readyReadStandardError()), this, SLOT(_readyReadStandardError())); + connect(&m_proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(_finished(int, QProcess::ExitStatus))); + connect(&m_proc, SIGNAL(error(QProcess::ProcessError)), this, SLOT(_error(QProcess::ProcessError))); +} + +ChildProcessContext::~ChildProcessContext() +{ +} + +// public: + +qint64 ChildProcessContext::pid() const +{ + Q_PID pid = m_proc.pid(); + +#if !defined(Q_OS_WIN) && !defined(Q_OS_WINCE) + return pid; +#else + return pid->dwProcessId; +#endif +} + +void ChildProcessContext::kill(const QString& signal) +{ + // TODO: it would be nice to be able to handle more signals + if ("SIGKILL" == signal) { + m_proc.kill(); + } else { + // Default to "SIGTERM" + m_proc.terminate(); + } +} + +void ChildProcessContext::_setEncoding(const QString& encoding) +{ + m_encoding.setEncoding(encoding); +} + +// This is affected by [QTBUG-5990](https://bugreports.qt-project.org/browse/QTBUG-5990). +// `QProcess` doesn't properly handle the situations of `cmd` not existing or +// failing to start... +bool ChildProcessContext::_start(const QString& cmd, const QStringList& args) +{ + m_proc.start(cmd, args); + // TODO: Is there a better way to do this??? + return m_proc.waitForStarted(1000); +} + +// private slots: + +void ChildProcessContext::_readyReadStandardOutput() +{ + QByteArray bytes = m_proc.readAllStandardOutput(); + emit stdoutData(m_encoding.decode(bytes)); +} + +void ChildProcessContext::_readyReadStandardError() +{ + QByteArray bytes = m_proc.readAllStandardError(); + emit stderrData(m_encoding.decode(bytes)); +} + +void ChildProcessContext::_finished(const int exitCode, const QProcess::ExitStatus exitStatus) +{ + Q_UNUSED(exitStatus) + + emit exit(exitCode); +} + +void ChildProcessContext::_error(const QProcess::ProcessError error) +{ + Q_UNUSED(error) + + emit exit(m_proc.exitCode()); +} + + +// +// ChildProcess +// + +ChildProcess::ChildProcess(QObject* parent) + : QObject(parent) +{ +} + +ChildProcess::~ChildProcess() +{ +} + +// public: + +QObject* ChildProcess::_createChildProcessContext() +{ + return new ChildProcessContext(this); +} diff --git a/src/childprocess.h b/src/childprocess.h new file mode 100644 index 0000000..0181743 --- /dev/null +++ b/src/childprocess.h @@ -0,0 +1,97 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CHILDPROCESS_H +#define CHILDPROCESS_H + +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +#include "encoding.h" + +/** + * This class wraps a QProcess and facilitates emulation of node.js's ChildProcess + */ +class ChildProcessContext : public QObject +{ + Q_OBJECT + Q_PROPERTY(qint64 pid READ pid) + +public: + explicit ChildProcessContext(QObject* parent = 0); + virtual ~ChildProcessContext(); + + qint64 pid() const; + Q_INVOKABLE void kill(const QString& signal = "SIGTERM"); + + Q_INVOKABLE void _setEncoding(const QString& encoding); + Q_INVOKABLE bool _start(const QString& cmd, const QStringList& args); + +signals: + void exit(const int code) const; + + /** + * For emulating `child.stdout.on("data", function (data) {})` + */ + void stdoutData(const QString& data) const; + /** + * For emulating `child.stderr.on("data", function (data) {})` + */ + void stderrData(const QString& data) const; + +private slots: + void _readyReadStandardOutput(); + void _readyReadStandardError(); + void _error(const QProcess::ProcessError error); + void _finished(const int exitCode, const QProcess::ExitStatus exitStatus); + +private: + QProcess m_proc; + Encoding m_encoding; +}; + +/** + * Helper class for child_process module + */ +class ChildProcess : public QObject +{ + Q_OBJECT + +public: + explicit ChildProcess(QObject* parent = 0); + virtual ~ChildProcess(); + + Q_INVOKABLE QObject* _createChildProcessContext(); +}; + +#endif // CHILDPROCESS_H diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..661d6c8 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,923 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 Ariya Hidayat + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + Copyright (C) 2013 James M. Greene + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "terminal.h" +#include "qcommandline.h" +#include "utils.h" +#include "consts.h" + +#include + + +static const struct QCommandLineConfigEntry flags[] = { + { QCommandLine::Option, '\0', "cookies-file", "Sets the file name to store the persistent cookies", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "config", "Specifies JSON-formatted configuration file", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "debug", "Prints additional warning and debug message: 'true' or 'false' (default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "disk-cache", "Enables disk cache: 'true' or 'false' (default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "disk-cache-path", "Specifies the location for the disk cache", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ignore-ssl-errors", "Ignores SSL errors (expired/self-signed certificate errors): 'true' or 'false' (default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "load-images", "Loads all inlined images: 'true' (default) or 'false'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "local-url-access", "Allows use of 'file:///' URLs: 'true' (default) or 'false'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "local-storage-path", "Specifies the location for local storage", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "local-storage-quota", "Sets the maximum size of the local storage (in KB)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "offline-storage-path", "Specifies the location for offline storage", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "offline-storage-quota", "Sets the maximum size of the offline storage (in KB)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "local-to-remote-url-access", "Allows local content to access remote URL: 'true' or 'false' (default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "max-disk-cache-size", "Limits the size of the disk cache (in KB)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "output-encoding", "Sets the encoding for the terminal output, default is 'utf8'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "remote-debugger-port", "Starts the script in a debug harness and listens on the specified port", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "remote-debugger-autorun", "Runs the script in the debugger immediately: 'true' or 'false' (default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "proxy", "Sets the proxy server, e.g. '--proxy=http://proxy.company.com:8080'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "proxy-auth", "Provides authentication information for the proxy, e.g. ''-proxy-auth=username:password'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "proxy-type", "Specifies the proxy type, 'http' (default), 'none' (disable completely), or 'socks5'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "script-encoding", "Sets the encoding used for the starting script, default is 'utf8'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "script-language", "Sets the script language instead of detecting it: 'javascript'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "web-security", "Enables web security, 'true' (default) or 'false'", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-protocol", "Selects a specific SSL protocol version to offer. Values (case insensitive): TLSv1.2, TLSv1.1, TLSv1.0, TLSv1 (same as v1.0), SSLv3, or ANY. Default is to offer all that Qt thinks are secure (SSLv3 and up). Not all values may be supported, depending on the system OpenSSL library.", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-ciphers", "Sets supported TLS/SSL ciphers. Argument is a colon-separated list of OpenSSL cipher names (macros like ALL, kRSA, etc. may not be used). Default matches modern browsers.", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-certificates-path", "Sets the location for custom CA certificates (if none set, uses environment variable SSL_CERT_DIR. If none set too, uses system default)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-client-certificate-file", "Sets the location of a client certificate", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-client-key-file", "Sets the location of a clients' private key", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "ssl-client-key-passphrase", "Sets the passphrase for the clients' private key", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver", "Starts in 'Remote WebDriver mode' (embedded GhostDriver): '[[:]]' (default '127.0.0.1:8910') ", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver-logfile", "File where to write the WebDriver's Log (default 'none') (NOTE: needs '--webdriver') ", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver-loglevel", "WebDriver Logging Level: (supported: 'ERROR', 'WARN', 'INFO', 'DEBUG') (default 'INFO') (NOTE: needs '--webdriver') ", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "webdriver-selenium-grid-hub", "URL to the Selenium Grid HUB: 'URL_TO_HUB' (default 'none') (NOTE: needs '--webdriver') ", QCommandLine::Optional }, + { QCommandLine::Param, '\0', "script", "Script", QCommandLine::Flags(QCommandLine::Optional | QCommandLine::ParameterFence)}, + { QCommandLine::Param, '\0', "argument", "Script argument", QCommandLine::OptionalMultiple }, + { QCommandLine::Switch, 'w', "wd", "Equivalent to '--webdriver' option above", QCommandLine::Optional }, + { QCommandLine::Switch, 'h', "help", "Shows this message and quits", QCommandLine::Optional }, + { QCommandLine::Switch, 'v', "version", "Prints out PhantomJS version", QCommandLine::Optional }, + QCOMMANDLINE_CONFIG_ENTRY_END +}; + +Config::Config(QObject* parent) + : QObject(parent) +{ + m_cmdLine = new QCommandLine(this); + + // We will handle --help and --version ourselves in phantom.cpp + m_cmdLine->enableHelp(false); + m_cmdLine->enableVersion(false); + + resetToDefaults(); +} + +void Config::init(const QStringList* const args) +{ + resetToDefaults(); + + QByteArray envSslCertDir = qgetenv("SSL_CERT_DIR"); + if (!envSslCertDir.isEmpty()) { + setSslCertificatesPath(envSslCertDir); + } + + processArgs(*args); +} + +void Config::processArgs(const QStringList& args) +{ + connect(m_cmdLine, SIGNAL(switchFound(const QString&)), this, SLOT(handleSwitch(const QString&))); + connect(m_cmdLine, SIGNAL(optionFound(const QString&, const QVariant&)), this, SLOT(handleOption(const QString&, const QVariant&))); + connect(m_cmdLine, SIGNAL(paramFound(const QString&, const QVariant&)), this, SLOT(handleParam(const QString&, const QVariant&))); + connect(m_cmdLine, SIGNAL(parseError(const QString&)), this, SLOT(handleError(const QString&))); + + m_cmdLine->setArguments(args); + m_cmdLine->setConfig(flags); + m_cmdLine->parse(); + + // Inject command line parameters to be picked up by GhostDriver + if (isWebdriverMode()) { + QStringList argsForGhostDriver; + + m_scriptFile = "main.js"; //< launch script + + argsForGhostDriver << QString("--ip=%1").arg(m_webdriverIp); //< "--ip=IP" + argsForGhostDriver << QString("--port=%1").arg(m_webdriverPort); //< "--port=PORT" + + if (!m_webdriverSeleniumGridHub.isEmpty()) { + argsForGhostDriver << QString("--hub=%1").arg(m_webdriverSeleniumGridHub); //< "--hub=SELENIUM_GRID_HUB_URL" + } + + if (!m_webdriverLogFile.isEmpty()) { + argsForGhostDriver << QString("--logFile=%1").arg(m_webdriverLogFile); //< "--logFile=LOG_FILE" + argsForGhostDriver << "--logColor=false"; //< Force no-color-output in Log File + } + + argsForGhostDriver << QString("--logLevel=%1").arg(m_webdriverLogLevel); //< "--logLevel=LOG_LEVEL" + + // Clear current args and override with those + setScriptArgs(argsForGhostDriver); + } +} + +void Config::loadJsonFile(const QString& filePath) +{ + QString jsonConfig; + QFile f(filePath); + + // Check file exists and is readable + if (!f.exists() || !f.open(QFile::ReadOnly | QFile::Text)) { + Terminal::instance()->cerr("Unable to open config: \"" + filePath + "\""); + return; + } + + // Read content + jsonConfig = QString::fromUtf8(f.readAll().trimmed()); + f.close(); + + // Check it's a valid JSON format + if (jsonConfig.isEmpty() || !jsonConfig.startsWith('{') || !jsonConfig.endsWith('}')) { + Terminal::instance()->cerr("Config file MUST be in JSON format!"); + return; + } + + // Load configurator + QString configurator = Utils::readResourceFileUtf8(":/configurator.js"); + + // Use a temporary QWebPage to load the JSON configuration in this Object using the 'configurator' above + QWebPage webPage; + // Add this object to the global scope + webPage.mainFrame()->addToJavaScriptWindowObject("config", this); + // Apply the JSON config settings to this very object + webPage.mainFrame()->evaluateJavaScript(configurator.arg(jsonConfig)); +} + +QString Config::helpText() const +{ + return m_cmdLine->help(); +} + +bool Config::autoLoadImages() const +{ + return m_autoLoadImages; +} + +void Config::setAutoLoadImages(const bool value) +{ + m_autoLoadImages = value; +} + +QString Config::cookiesFile() const +{ + return m_cookiesFile; +} + +void Config::setCookiesFile(const QString& value) +{ + m_cookiesFile = value; +} + +QString Config::offlineStoragePath() const +{ + return m_offlineStoragePath; +} + +void Config::setOfflineStoragePath(const QString& value) +{ + QDir dir(value); + m_offlineStoragePath = dir.absolutePath(); +} + +int Config::offlineStorageDefaultQuota() const +{ + return m_offlineStorageDefaultQuota; +} + +void Config::setOfflineStorageDefaultQuota(int offlineStorageDefaultQuota) +{ + m_offlineStorageDefaultQuota = offlineStorageDefaultQuota * 1024; +} + + +QString Config::localStoragePath() const +{ + return m_localStoragePath; +} + +void Config::setLocalStoragePath(const QString& value) +{ + QDir dir(value); + m_localStoragePath = dir.absolutePath(); +} + +int Config::localStorageDefaultQuota() const +{ + return m_localStorageDefaultQuota; +} + +void Config::setLocalStorageDefaultQuota(int localStorageDefaultQuota) +{ + m_localStorageDefaultQuota = localStorageDefaultQuota * 1024; +} + +bool Config::diskCacheEnabled() const +{ + return m_diskCacheEnabled; +} + +void Config::setDiskCacheEnabled(const bool value) +{ + m_diskCacheEnabled = value; +} + +int Config::maxDiskCacheSize() const +{ + return m_maxDiskCacheSize; +} + +void Config::setMaxDiskCacheSize(int maxDiskCacheSize) +{ + m_maxDiskCacheSize = maxDiskCacheSize; +} + +QString Config::diskCachePath() const +{ + return m_diskCachePath; +} + +void Config::setDiskCachePath(const QString& value) +{ + QDir dir(value); + m_diskCachePath = dir.absolutePath(); +} + +bool Config::ignoreSslErrors() const +{ + return m_ignoreSslErrors; +} + +void Config::setIgnoreSslErrors(const bool value) +{ + m_ignoreSslErrors = value; +} + +bool Config::localUrlAccessEnabled() const +{ + return m_localUrlAccessEnabled; +} + +void Config::setLocalUrlAccessEnabled(const bool value) +{ + m_localUrlAccessEnabled = value; +} + +bool Config::localToRemoteUrlAccessEnabled() const +{ + return m_localToRemoteUrlAccessEnabled; +} + +void Config::setLocalToRemoteUrlAccessEnabled(const bool value) +{ + m_localToRemoteUrlAccessEnabled = value; +} + +QString Config::outputEncoding() const +{ + return m_outputEncoding; +} + +void Config::setOutputEncoding(const QString& value) +{ + if (value.isEmpty()) { + return; + } + + m_outputEncoding = value; +} + +QString Config::proxyType() const +{ + return m_proxyType; +} + +void Config::setProxyType(const QString& value) +{ + m_proxyType = value; +} + +QString Config::proxy() const +{ + return m_proxyHost + ":" + QString::number(m_proxyPort); +} + +void Config::setProxy(const QString& value) +{ + QUrl proxyUrl = QUrl::fromUserInput(value); + + if (proxyUrl.isValid()) { + setProxyHost(proxyUrl.host()); + setProxyPort(proxyUrl.port(1080)); + } +} + +void Config::setProxyAuth(const QString& value) +{ + QString proxyUser = value; + QString proxyPass = ""; + + if (proxyUser.lastIndexOf(':') > 0) { + proxyPass = proxyUser.mid(proxyUser.lastIndexOf(':') + 1).trimmed(); + proxyUser = proxyUser.left(proxyUser.lastIndexOf(':')).trimmed(); + + setProxyAuthUser(proxyUser); + setProxyAuthPass(proxyPass); + } +} + +QString Config::proxyAuth() const +{ + return proxyAuthUser() + ":" + proxyAuthPass(); +} + +QString Config::proxyAuthUser() const +{ + return m_proxyAuthUser; +} + +QString Config::proxyAuthPass() const +{ + return m_proxyAuthPass; +} + +QString Config::proxyHost() const +{ + return m_proxyHost; +} + +int Config::proxyPort() const +{ + return m_proxyPort; +} + +QStringList Config::scriptArgs() const +{ + return m_scriptArgs; +} + +void Config::setScriptArgs(const QStringList& value) +{ + m_scriptArgs.clear(); + + QStringListIterator it(value); + while (it.hasNext()) { + m_scriptArgs.append(it.next()); + } +} + +QString Config::scriptEncoding() const +{ + return m_scriptEncoding; +} + +void Config::setScriptEncoding(const QString& value) +{ + if (value.isEmpty()) { + return; + } + + m_scriptEncoding = value; +} + +QString Config::scriptLanguage() const +{ + return m_scriptLanguage; +} + +void Config::setScriptLanguage(const QString& value) +{ + if (value.isEmpty()) { + return; + } + + m_scriptLanguage = value; +} + +QString Config::scriptFile() const +{ + return m_scriptFile; +} + +void Config::setScriptFile(const QString& value) +{ + m_scriptFile = value; +} + +QString Config::unknownOption() const +{ + return m_unknownOption; +} + +void Config::setUnknownOption(const QString& value) +{ + m_unknownOption = value; +} + +bool Config::versionFlag() const +{ + return m_versionFlag; +} + +void Config::setVersionFlag(const bool value) +{ + m_versionFlag = value; +} + +bool Config::debug() const +{ + return m_debug; +} + +void Config::setDebug(const bool value) +{ + m_debug = value; +} + +int Config::remoteDebugPort() const +{ + return m_remoteDebugPort; +} + +void Config::setRemoteDebugPort(const int port) +{ + m_remoteDebugPort = port; +} + +bool Config::remoteDebugAutorun() const +{ + return m_remoteDebugAutorun; +} + +void Config::setRemoteDebugAutorun(const bool value) +{ + m_remoteDebugAutorun = value; +} + +bool Config::webSecurityEnabled() const +{ + return m_webSecurityEnabled; +} + +void Config::setWebSecurityEnabled(const bool value) +{ + m_webSecurityEnabled = value; +} + +void Config::setJavascriptCanOpenWindows(const bool value) +{ + m_javascriptCanOpenWindows = value; +} + +bool Config::javascriptCanOpenWindows() const +{ + return m_javascriptCanOpenWindows; +} + +void Config::setJavascriptCanCloseWindows(const bool value) +{ + m_javascriptCanCloseWindows = value; +} + +bool Config::javascriptCanCloseWindows() const +{ + return m_javascriptCanCloseWindows; +} + +void Config::setWebdriver(const QString& webdriverConfig) +{ + // Parse and validate the configuration + bool isValidPort; + QStringList wdCfg = webdriverConfig.split(':'); + if (wdCfg.length() == 1 && wdCfg[0].toInt(&isValidPort) && isValidPort) { + // Only a PORT was provided + m_webdriverPort = wdCfg[0]; + } else if (wdCfg.length() == 2 && !wdCfg[0].isEmpty() && wdCfg[1].toInt(&isValidPort) && isValidPort) { + // Both IP and PORT provided + m_webdriverIp = wdCfg[0]; + m_webdriverPort = wdCfg[1]; + } +} + +QString Config::webdriver() const +{ + return QString("%1:%2").arg(m_webdriverIp).arg(m_webdriverPort); +} + +bool Config::isWebdriverMode() const +{ + return !m_webdriverPort.isEmpty(); +} + +void Config::setWebdriverLogFile(const QString& webdriverLogFile) +{ + m_webdriverLogFile = webdriverLogFile; +} + +QString Config::webdriverLogFile() const +{ + return m_webdriverLogFile; +} + +void Config::setWebdriverLogLevel(const QString& webdriverLogLevel) +{ + m_webdriverLogLevel = webdriverLogLevel; +} + +QString Config::webdriverLogLevel() const +{ + return m_webdriverLogLevel; +} + +void Config::setWebdriverSeleniumGridHub(const QString& hubUrl) +{ + m_webdriverSeleniumGridHub = hubUrl; +} + +QString Config::webdriverSeleniumGridHub() const +{ + return m_webdriverSeleniumGridHub; +} + +// private: +void Config::resetToDefaults() +{ + m_autoLoadImages = true; + m_cookiesFile = QString(); + m_offlineStoragePath = QString(); + m_offlineStorageDefaultQuota = -1; + m_localStoragePath = QString(); + m_localStorageDefaultQuota = -1; + m_diskCacheEnabled = false; + m_maxDiskCacheSize = -1; + m_diskCachePath = QString(); + m_ignoreSslErrors = false; + m_localUrlAccessEnabled = true; + m_localToRemoteUrlAccessEnabled = false; + m_outputEncoding = "UTF-8"; + m_proxyType = "http"; + m_proxyHost.clear(); + m_proxyPort = 1080; + m_proxyAuthUser.clear(); + m_proxyAuthPass.clear(); + m_scriptArgs.clear(); + m_scriptEncoding = "UTF-8"; + m_scriptLanguage.clear(); + m_scriptFile.clear(); + m_unknownOption.clear(); + m_versionFlag = false; + m_debug = false; + m_remoteDebugPort = -1; + m_remoteDebugAutorun = false; + m_webSecurityEnabled = true; + m_javascriptCanOpenWindows = true; + m_javascriptCanCloseWindows = true; + m_helpFlag = false; + m_printDebugMessages = false; + m_sslProtocol = "default"; + // Default taken from Chromium 35.0.1916.153 + m_sslCiphers = ("ECDHE-ECDSA-AES128-GCM-SHA256" + ":ECDHE-RSA-AES128-GCM-SHA256" + ":DHE-RSA-AES128-GCM-SHA256" + ":ECDHE-ECDSA-AES256-SHA" + ":ECDHE-ECDSA-AES128-SHA" + ":ECDHE-RSA-AES128-SHA" + ":ECDHE-RSA-AES256-SHA" + ":ECDHE-ECDSA-RC4-SHA" + ":ECDHE-RSA-RC4-SHA" + ":DHE-RSA-AES128-SHA" + ":DHE-DSS-AES128-SHA" + ":DHE-RSA-AES256-SHA" + ":AES128-GCM-SHA256" + ":AES128-SHA" + ":AES256-SHA" + ":DES-CBC3-SHA" + ":RC4-SHA" + ":RC4-MD5"); + m_sslCertificatesPath.clear(); + m_sslClientCertificateFile.clear(); + m_sslClientKeyFile.clear(); + m_sslClientKeyPassphrase.clear(); + m_webdriverIp = QString(); + m_webdriverPort = QString(); + m_webdriverLogFile = QString(); + m_webdriverLogLevel = "INFO"; + m_webdriverSeleniumGridHub = QString(); +} + +void Config::setProxyAuthPass(const QString& value) +{ + m_proxyAuthPass = value; +} + +void Config::setProxyAuthUser(const QString& value) +{ + m_proxyAuthUser = value; +} + +void Config::setProxyHost(const QString& value) +{ + m_proxyHost = value; +} + +void Config::setProxyPort(const int value) +{ + m_proxyPort = value; +} + +bool Config::helpFlag() const +{ + return m_helpFlag; +} + +void Config::setHelpFlag(const bool value) +{ + m_helpFlag = value; +} + +bool Config::printDebugMessages() const +{ + return m_printDebugMessages; +} + +void Config::setPrintDebugMessages(const bool value) +{ + m_printDebugMessages = value; +} + +void Config::handleSwitch(const QString& sw) +{ + setHelpFlag(sw == "help"); + setVersionFlag(sw == "version"); + + if (sw == "wd") { + setWebdriver(DEFAULT_WEBDRIVER_CONFIG); + } +} + +void Config::handleOption(const QString& option, const QVariant& value) +{ + bool boolValue = false; + + QStringList booleanFlags; + booleanFlags << "debug"; + booleanFlags << "disk-cache"; + booleanFlags << "ignore-ssl-errors"; + booleanFlags << "load-images"; + booleanFlags << "local-url-access"; + booleanFlags << "local-to-remote-url-access"; + booleanFlags << "remote-debugger-autorun"; + booleanFlags << "web-security"; + if (booleanFlags.contains(option)) { + if ((value != "true") && (value != "yes") && (value != "false") && (value != "no")) { + setUnknownOption(QString("Invalid values for '%1' option.").arg(option)); + return; + } + boolValue = (value == "true") || (value == "yes"); + } + + if (option == "cookies-file") { + setCookiesFile(value.toString()); + } + + if (option == "config") { + loadJsonFile(value.toString()); + } + + if (option == "debug") { + setPrintDebugMessages(boolValue); + } + + if (option == "disk-cache") { + setDiskCacheEnabled(boolValue); + } + + if (option == "disk-cache-path") { + setDiskCachePath(value.toString()); + } + + if (option == "ignore-ssl-errors") { + setIgnoreSslErrors(boolValue); + } + + if (option == "load-images") { + setAutoLoadImages(boolValue); + } + + if (option == "local-storage-path") { + setLocalStoragePath(value.toString()); + } + + if (option == "local-storage-quota") { + setLocalStorageDefaultQuota(value.toInt()); + } + + if (option == "offline-storage-path") { + setOfflineStoragePath(value.toString()); + } + + if (option == "offline-storage-quota") { + setOfflineStorageDefaultQuota(value.toInt()); + } + + if (option == "local-url-access") { + setLocalUrlAccessEnabled(boolValue); + } + + if (option == "local-to-remote-url-access") { + setLocalToRemoteUrlAccessEnabled(boolValue); + } + + if (option == "max-disk-cache-size") { + setMaxDiskCacheSize(value.toInt()); + } + + if (option == "output-encoding") { + setOutputEncoding(value.toString()); + } + + if (option == "remote-debugger-autorun") { + setRemoteDebugAutorun(boolValue); + } + + if (option == "remote-debugger-port") { + setDebug(true); + setRemoteDebugPort(value.toInt()); + } + + if (option == "proxy") { + setProxy(value.toString()); + } + + if (option == "proxy-type") { + setProxyType(value.toString()); + } + + if (option == "proxy-auth") { + setProxyAuth(value.toString()); + } + + if (option == "script-encoding") { + setScriptEncoding(value.toString()); + } + + if (option == "script-language") { + setScriptLanguage(value.toString()); + } + + if (option == "web-security") { + setWebSecurityEnabled(boolValue); + } + if (option == "ssl-protocol") { + setSslProtocol(value.toString()); + } + if (option == "ssl-ciphers") { + setSslCiphers(value.toString()); + } + if (option == "ssl-certificates-path") { + setSslCertificatesPath(value.toString()); + } + if (option == "ssl-client-certificate-file") { + setSslClientCertificateFile(value.toString()); + } + if (option == "ssl-client-key-file") { + setSslClientKeyFile(value.toString()); + } + if (option == "ssl-client-key-passphrase") { + setSslClientKeyPassphrase(value.toByteArray()); + } + if (option == "webdriver") { + setWebdriver(value.toString().length() > 0 ? value.toString() : DEFAULT_WEBDRIVER_CONFIG); + } + if (option == "webdriver-logfile") { + setWebdriverLogFile(value.toString()); + } + if (option == "webdriver-loglevel") { + setWebdriverLogLevel(value.toString()); + } + if (option == "webdriver-selenium-grid-hub") { + setWebdriverSeleniumGridHub(value.toString()); + } +} + +void Config::handleParam(const QString& param, const QVariant& value) +{ + Q_UNUSED(param); + + if (m_scriptFile.isEmpty()) { + m_scriptFile = value.toString(); + } else { + m_scriptArgs += value.toString(); + } +} + +void Config::handleError(const QString& error) +{ + setUnknownOption(QString("Error: %1").arg(error)); +} + +QString Config::sslProtocol() const +{ + return m_sslProtocol; +} + +void Config::setSslProtocol(const QString& sslProtocolName) +{ + m_sslProtocol = sslProtocolName.toLower(); +} + +QString Config::sslCiphers() const +{ + return m_sslCiphers; +} + +void Config::setSslCiphers(const QString& sslCiphersName) +{ + // OpenSSL cipher strings are case sensitive. + m_sslCiphers = sslCiphersName; +} + +QString Config::sslCertificatesPath() const +{ + return m_sslCertificatesPath; +} + +void Config::setSslCertificatesPath(const QString& sslCertificatesPath) +{ + QFileInfo sslPathInfo = QFileInfo(sslCertificatesPath); + if (sslPathInfo.isDir()) { + if (sslCertificatesPath.endsWith('/')) { + m_sslCertificatesPath = sslCertificatesPath + "*"; + } else { + m_sslCertificatesPath = sslCertificatesPath + "/*"; + } + } else { + m_sslCertificatesPath = sslCertificatesPath; + } +} + +QString Config::sslClientCertificateFile() const +{ + return m_sslClientCertificateFile; +} + +void Config::setSslClientCertificateFile(const QString& sslClientCertificateFile) +{ + m_sslClientCertificateFile = sslClientCertificateFile; +} + +QString Config::sslClientKeyFile() const +{ + return m_sslClientKeyFile; +} + +void Config::setSslClientKeyFile(const QString& sslClientKeyFile) +{ + m_sslClientKeyFile = sslClientKeyFile; +} + +QByteArray Config::sslClientKeyPassphrase() const +{ + return m_sslClientKeyPassphrase; +} + +void Config::setSslClientKeyPassphrase(const QByteArray& sslClientKeyPassphrase) +{ + m_sslClientKeyPassphrase = sslClientKeyPassphrase; +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..9fda022 --- /dev/null +++ b/src/config.h @@ -0,0 +1,272 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include +#include + +class QCommandLine; + +class Config: public QObject +{ + Q_OBJECT + Q_PROPERTY(QString cookiesFile READ cookiesFile WRITE setCookiesFile) + Q_PROPERTY(bool diskCacheEnabled READ diskCacheEnabled WRITE setDiskCacheEnabled) + Q_PROPERTY(int maxDiskCacheSize READ maxDiskCacheSize WRITE setMaxDiskCacheSize) + Q_PROPERTY(QString diskCachePath READ diskCachePath WRITE setDiskCachePath) + Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors) + Q_PROPERTY(bool localUrlAccessEnabled READ localUrlAccessEnabled WRITE setLocalUrlAccessEnabled) + Q_PROPERTY(bool localToRemoteUrlAccessEnabled READ localToRemoteUrlAccessEnabled WRITE setLocalToRemoteUrlAccessEnabled) + Q_PROPERTY(QString outputEncoding READ outputEncoding WRITE setOutputEncoding) + Q_PROPERTY(QString proxyType READ proxyType WRITE setProxyType) + Q_PROPERTY(QString proxy READ proxy WRITE setProxy) + Q_PROPERTY(QString proxyAuth READ proxyAuth WRITE setProxyAuth) + Q_PROPERTY(QString scriptEncoding READ scriptEncoding WRITE setScriptEncoding) + Q_PROPERTY(bool webSecurityEnabled READ webSecurityEnabled WRITE setWebSecurityEnabled) + Q_PROPERTY(QString offlineStoragePath READ offlineStoragePath WRITE setOfflineStoragePath) + Q_PROPERTY(QString localStoragePath READ localStoragePath WRITE setLocalStoragePath) + Q_PROPERTY(int localStorageDefaultQuota READ localStorageDefaultQuota WRITE setLocalStorageDefaultQuota) + Q_PROPERTY(int offlineStorageDefaultQuota READ offlineStorageDefaultQuota WRITE setOfflineStorageDefaultQuota) + Q_PROPERTY(bool printDebugMessages READ printDebugMessages WRITE setPrintDebugMessages) + Q_PROPERTY(bool javascriptCanOpenWindows READ javascriptCanOpenWindows WRITE setJavascriptCanOpenWindows) + Q_PROPERTY(bool javascriptCanCloseWindows READ javascriptCanCloseWindows WRITE setJavascriptCanCloseWindows) + Q_PROPERTY(QString sslProtocol READ sslProtocol WRITE setSslProtocol) + Q_PROPERTY(QString sslCiphers READ sslCiphers WRITE setSslCiphers) + Q_PROPERTY(QString sslCertificatesPath READ sslCertificatesPath WRITE setSslCertificatesPath) + Q_PROPERTY(QString sslClientCertificateFile READ sslClientCertificateFile WRITE setSslClientCertificateFile) + Q_PROPERTY(QString sslClientKeyFile READ sslClientKeyFile WRITE setSslClientKeyFile) + Q_PROPERTY(QByteArray sslClientKeyPassphrase READ sslClientKeyPassphrase WRITE setSslClientKeyPassphrase) + Q_PROPERTY(QString webdriver READ webdriver WRITE setWebdriver) + Q_PROPERTY(QString webdriverLogFile READ webdriverLogFile WRITE setWebdriverLogFile) + Q_PROPERTY(QString webdriverLogLevel READ webdriverLogLevel WRITE setWebdriverLogLevel) + Q_PROPERTY(QString webdriverSeleniumGridHub READ webdriverSeleniumGridHub WRITE setWebdriverSeleniumGridHub) + +public: + Config(QObject* parent = 0); + + void init(const QStringList* const args); + void processArgs(const QStringList& args); + void loadJsonFile(const QString& filePath); + + QString helpText() const; + + bool autoLoadImages() const; + void setAutoLoadImages(const bool value); + + QString cookiesFile() const; + void setCookiesFile(const QString& cookiesFile); + + QString offlineStoragePath() const; + void setOfflineStoragePath(const QString& value); + + int offlineStorageDefaultQuota() const; + void setOfflineStorageDefaultQuota(int offlineStorageDefaultQuota); + + QString localStoragePath() const; + void setLocalStoragePath(const QString& value); + + int localStorageDefaultQuota() const; + void setLocalStorageDefaultQuota(int localStorageDefaultQuota); + + bool diskCacheEnabled() const; + void setDiskCacheEnabled(const bool value); + + int maxDiskCacheSize() const; + void setMaxDiskCacheSize(int maxDiskCacheSize); + + QString diskCachePath() const; + void setDiskCachePath(const QString& value); + + bool ignoreSslErrors() const; + void setIgnoreSslErrors(const bool value); + + bool localUrlAccessEnabled() const; + void setLocalUrlAccessEnabled(const bool value); + + bool localToRemoteUrlAccessEnabled() const; + void setLocalToRemoteUrlAccessEnabled(const bool value); + + QString outputEncoding() const; + void setOutputEncoding(const QString& value); + + QString proxyType() const; + void setProxyType(const QString& value); + + QString proxy() const; + void setProxy(const QString& value); + QString proxyHost() const; + int proxyPort() const; + + QString proxyAuth() const; + void setProxyAuth(const QString& value); + QString proxyAuthUser() const; + QString proxyAuthPass() const; + void setProxyAuthUser(const QString& value); + void setProxyAuthPass(const QString& value); + + QStringList scriptArgs() const; + void setScriptArgs(const QStringList& value); + + QString scriptEncoding() const; + void setScriptEncoding(const QString& value); + + QString scriptLanguage() const; + void setScriptLanguage(const QString& value); + + QString scriptFile() const; + void setScriptFile(const QString& value); + + QString unknownOption() const; + void setUnknownOption(const QString& value); + + bool versionFlag() const; + void setVersionFlag(const bool value); + + void setDebug(const bool value); + bool debug() const; + + void setRemoteDebugPort(const int port); + int remoteDebugPort() const; + + void setRemoteDebugAutorun(const bool value); + bool remoteDebugAutorun() const; + + bool webSecurityEnabled() const; + void setWebSecurityEnabled(const bool value); + + bool helpFlag() const; + void setHelpFlag(const bool value); + + void setPrintDebugMessages(const bool value); + bool printDebugMessages() const; + + void setJavascriptCanOpenWindows(const bool value); + bool javascriptCanOpenWindows() const; + + void setJavascriptCanCloseWindows(const bool value); + bool javascriptCanCloseWindows() const; + + void setSslProtocol(const QString& sslProtocolName); + QString sslProtocol() const; + + void setSslCiphers(const QString& sslCiphersName); + QString sslCiphers() const; + + void setSslCertificatesPath(const QString& sslCertificatesPath); + QString sslCertificatesPath() const; + + void setSslClientCertificateFile(const QString& sslClientCertificateFile); + QString sslClientCertificateFile() const; + + void setSslClientKeyFile(const QString& sslClientKeyFile); + QString sslClientKeyFile() const; + + void setSslClientKeyPassphrase(const QByteArray& sslClientKeyPassphrase); + QByteArray sslClientKeyPassphrase() const; + + void setWebdriver(const QString& webdriverConfig); + QString webdriver() const; + bool isWebdriverMode() const; + + void setWebdriverLogFile(const QString& webdriverLogFile); + QString webdriverLogFile() const; + + void setWebdriverLogLevel(const QString& webdriverLogLevel); + QString webdriverLogLevel() const; + + void setWebdriverSeleniumGridHub(const QString& hubUrl); + QString webdriverSeleniumGridHub() const; + +public slots: + void handleSwitch(const QString& sw); + void handleOption(const QString& option, const QVariant& value); + void handleParam(const QString& param, const QVariant& value); + void handleError(const QString& error); + +private: + void resetToDefaults(); + void setProxyHost(const QString& value); + void setProxyPort(const int value); + void setAuthUser(const QString& value); + void setAuthPass(const QString& value); + + QCommandLine* m_cmdLine; + bool m_autoLoadImages; + QString m_cookiesFile; + QString m_offlineStoragePath; + int m_offlineStorageDefaultQuota; + QString m_localStoragePath; + int m_localStorageDefaultQuota; + bool m_diskCacheEnabled; + int m_maxDiskCacheSize; + QString m_diskCachePath; + bool m_ignoreSslErrors; + bool m_localUrlAccessEnabled; + bool m_localToRemoteUrlAccessEnabled; + QString m_outputEncoding; + QString m_proxyType; + QString m_proxyHost; + int m_proxyPort; + QString m_proxyAuthUser; + QString m_proxyAuthPass; + QStringList m_scriptArgs; + QString m_scriptEncoding; + QString m_scriptLanguage; + QString m_scriptFile; + QString m_unknownOption; + bool m_versionFlag; + QString m_authUser; + QString m_authPass; + bool m_debug; + int m_remoteDebugPort; + bool m_remoteDebugAutorun; + bool m_webSecurityEnabled; + bool m_helpFlag; + bool m_printDebugMessages; + bool m_javascriptCanOpenWindows; + bool m_javascriptCanCloseWindows; + QString m_sslProtocol; + QString m_sslCiphers; + QString m_sslCertificatesPath; + QString m_sslClientCertificateFile; + QString m_sslClientKeyFile; + QByteArray m_sslClientKeyPassphrase; + QString m_webdriverIp; + QString m_webdriverPort; + QString m_webdriverLogFile; + QString m_webdriverLogLevel; + QString m_webdriverSeleniumGridHub; +}; + +#endif // CONFIG_H diff --git a/src/configurator.js b/src/configurator.js new file mode 100644 index 0000000..36174ad --- /dev/null +++ b/src/configurator.js @@ -0,0 +1,17 @@ +(function (opts) { + var i; + + opts = opts || {}; + + if (typeof opts !== 'object') { + return; + } + + for (i in opts) { + if (opts.hasOwnProperty(i) && typeof opts[i] !== 'undefined') { + config[i] = opts[i]; + } + } + + return null; +})((%1)); diff --git a/src/consts.h b/src/consts.h new file mode 100644 index 0000000..b3a6e99 --- /dev/null +++ b/src/consts.h @@ -0,0 +1,72 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 Ivan De Marino + Copyright (C) 2011 James Roe + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CONSTS_H +#define CONSTS_H + +#define PHANTOMJS_VERSION_MAJOR 2 +#define PHANTOMJS_VERSION_MINOR 1 +#define PHANTOMJS_VERSION_PATCH 1 +#define PHANTOMJS_VERSION_STRING "2.1.1" + +#define HTTP_HEADER_CONTENT_LENGTH "content-length" +#define HTTP_HEADER_CONTENT_TYPE "content-type" + +#define JAVASCRIPT_SOURCE_PLATFORM_URL "phantomjs://platform/%1" +#define JAVASCRIPT_SOURCE_CODE_URL "phantomjs://code/%1" + +#define JS_ELEMENT_CLICK "(function (el) { " \ + "var ev = document.createEvent('MouseEvents');" \ + "ev.initEvent(\"click\", true, true);" \ + "el.dispatchEvent(ev);" \ + "})(this);" + +#define JS_APPEND_SCRIPT_ELEMENT "var el = document.createElement('script');" \ + "el.onload = function() { alert('%1'); };" \ + "el.src = '%1';" \ + "document.body.appendChild(el);" + +#define PAGE_SETTINGS_LOAD_IMAGES "loadImages" +#define PAGE_SETTINGS_JS_ENABLED "javascriptEnabled" +#define PAGE_SETTINGS_XSS_AUDITING "XSSAuditingEnabled" +#define PAGE_SETTINGS_USER_AGENT "userAgent" +#define PAGE_SETTINGS_PROXY "proxy" +#define PAGE_SETTINGS_LOCAL_ACCESS_REMOTE "localToRemoteUrlAccessEnabled" +#define PAGE_SETTINGS_USERNAME "userName" +#define PAGE_SETTINGS_PASSWORD "password" +#define PAGE_SETTINGS_MAX_AUTH_ATTEMPTS "maxAuthAttempts" +#define PAGE_SETTINGS_RESOURCE_TIMEOUT "resourceTimeout" +#define PAGE_SETTINGS_JS_CAN_OPEN_WINDOWS "javascriptCanOpenWindows" +#define PAGE_SETTINGS_JS_CAN_CLOSE_WINDOWS "javascriptCanCloseWindows" + +#define DEFAULT_WEBDRIVER_CONFIG "127.0.0.1:8910" + +#endif // CONSTS_H diff --git a/src/cookiejar.cpp b/src/cookiejar.cpp new file mode 100644 index 0000000..371d7c5 --- /dev/null +++ b/src/cookiejar.cpp @@ -0,0 +1,505 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2012 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "phantom.h" +#include "config.h" +#include "cookiejar.h" + +#include +#include +#include +#include + +#define COOKIE_JAR_VERSION 1 + +// Operators needed for Cookie Serialization +QT_BEGIN_NAMESPACE +QDataStream& operator<<(QDataStream& stream, const QList& list) +{ + stream << COOKIE_JAR_VERSION; + stream << quint32(list.size()); + for (int i = 0; i < list.size(); ++i) { + stream << list.at(i).toRawForm(); + } + return stream; +} + +QDataStream& operator>>(QDataStream& stream, QList& list) +{ + list.clear(); + + quint32 version; + stream >> version; + + if (version != COOKIE_JAR_VERSION) { + return stream; + } + + quint32 count; + stream >> count; + for (quint32 i = 0; i < count; ++i) { + QByteArray value; + stream >> value; + QList newCookies = QNetworkCookie::parseCookies(value); + if (newCookies.count() == 0 && value.length() != 0) { + qWarning() << "CookieJar: Unable to parse saved cookie:" << value; + } + for (int j = 0; j < newCookies.count(); ++j) { + list.append(newCookies.at(j)); + } + if (stream.atEnd()) { + break; + } + } + return stream; +} +QT_END_NAMESPACE + +// public: +CookieJar::CookieJar(QString cookiesFile, QObject* parent) + : QNetworkCookieJar(parent) + , m_enabled(true) +{ + if (cookiesFile == "") { + m_cookieStorage = 0; + qDebug() << "CookieJar - Created but will not store cookies (use option '--cookies-file=' to enable persistent cookie storage)"; + } else { + m_cookieStorage = new QSettings(cookiesFile, QSettings::IniFormat, this); + load(); + qDebug() << "CookieJar - Created and will store cookies in:" << cookiesFile; + } +} + +// private: +CookieJar::~CookieJar() +{ + // On destruction, before saving, clear all the session cookies + purgeSessionCookies(); + save(); +} + +bool CookieJar::setCookiesFromUrl(const QList& cookieList, const QUrl& url) +{ + // Update cookies in memory + if (isEnabled()) { + QNetworkCookieJar::setCookiesFromUrl(cookieList, url); + save(); + } + // No changes occurred + return false; +} + +QList CookieJar::cookiesForUrl(const QUrl& url) const +{ + if (isEnabled()) { + return QNetworkCookieJar::cookiesForUrl(url); + } + // The CookieJar is disabled: don't return any cookie + return QList(); +} + +bool CookieJar::addCookie(const QNetworkCookie& cookie, const QString& url) +{ + if (isEnabled() && (!url.isEmpty() || !cookie.domain().isEmpty())) { + // Save a single cookie + setCookiesFromUrl( + QList() << cookie, //< unfortunately, "setCookiesFromUrl" requires a list + !url.isEmpty() ? + url : //< use given URL + QString( //< mock-up a URL + (cookie.isSecure() ? "https://" : "http://") + //< URL protocol + QString(cookie.domain().startsWith('.') ? "www" : "") + cookie.domain() + //< URL domain + (cookie.path().isEmpty() ? "/" : cookie.path()))); //< URL path + + // Return "true" if the cookie was really set + if (contains(cookie)) { + return true; + } + qDebug() << "CookieJar - Rejected Cookie" << cookie.toRawForm(); + return false; + } + return false; +} + +void CookieJar::addCookie(const QVariantMap& cookie) +{ + addCookieFromMap(cookie); +} + +bool CookieJar::addCookieFromMap(const QVariantMap& cookie, const QString& url) +{ + QNetworkCookie newCookie; + + // The cookie must have a non-empty "name" and a "value" + if (!cookie["name"].isNull() && !cookie["name"].toString().isEmpty() && !cookie["value"].isNull()) { + // Name & Value + newCookie.setName(cookie["name"].toByteArray()); + newCookie.setValue(cookie["value"].toByteArray()); + + // Domain, if provided + if (!cookie["domain"].isNull() && !cookie["domain"].toString().isEmpty()) { + newCookie.setDomain(cookie["domain"].toString()); + } + + // Path, if provided + if (!cookie["path"].isNull() || !cookie["path"].toString().isEmpty()) { + newCookie.setPath(cookie["path"].toString()); + } + + // HttpOnly, false by default + newCookie.setHttpOnly(cookie["httponly"].isNull() ? false : cookie["httponly"].toBool()); + // Secure, false by default + newCookie.setSecure(cookie["secure"].isNull() ? false : cookie["secure"].toBool()); + + // Expiration Date, if provided, giving priority to "expires" over "expiry" + QVariant expiresVar; + if (!cookie["expires"].isNull()) { + expiresVar = cookie["expires"]; + } else if (!cookie["expiry"].isNull()) { + expiresVar = cookie["expiry"]; + } + + if (expiresVar.isValid()) { + QDateTime expirationDate; + if (expiresVar.type() == QVariant::String) { + // Set cookie expire date via "classic" string format + QString datetime = expiresVar.toString().replace(" GMT", ""); + expirationDate = QDateTime::fromString(datetime, "ddd, dd MMM yyyy hh:mm:ss"); + } else if (expiresVar.type() == QVariant::Double) { + // Set cookie expire date via "number of seconds since epoch" + // NOTE: Every JS number is a Double. + // @see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf + expirationDate = QDateTime::fromMSecsSinceEpoch(expiresVar.toLongLong()); + } + + if (expirationDate.isValid()) { + newCookie.setExpirationDate(expirationDate); + } + } + + return addCookie(newCookie, url); + } + return false; +} + +bool CookieJar::addCookies(const QList& cookiesList, const QString& url) +{ + bool added = false; + for (int i = cookiesList.length() - 1; i >= 0; --i) { + if (addCookie(cookiesList.at(i), url)) { + // change it to "true" if at least 1 cookie was set + added = true; + } + } + return added; +} + +bool CookieJar::addCookiesFromMap(const QVariantList& cookiesList, const QString& url) +{ + bool added = false; + for (int i = cookiesList.length() - 1; i >= 0; --i) { + if (addCookieFromMap(cookiesList.at(i).toMap(), url)) { + // change it to "true" if at least 1 cookie was set + added = true; + } + } + return added; +} + +QList CookieJar::cookies(const QString& url) const +{ + if (url.isEmpty()) { + // No url provided: return all the cookies in this CookieJar + return allCookies(); + } else { + // Return ONLY the cookies that match this URL + return cookiesForUrl(url); + } +} + +QVariantList CookieJar::cookiesToMap(const QString& url) const +{ + QVariantList result; + QNetworkCookie c; + QVariantMap cookie; + + QList cookiesList = cookies(url); + for (int i = cookiesList.length() - 1; i >= 0; --i) { + c = cookiesList.at(i); + + cookie.clear(); + cookie["domain"] = QVariant(c.domain()); + cookie["name"] = QVariant(QString(c.name())); + cookie["value"] = QVariant(QString(c.value())); + cookie["path"] = (c.path().isNull() || c.path().isEmpty()) ? QVariant("/") : QVariant(c.path()); + cookie["httponly"] = QVariant(c.isHttpOnly()); + cookie["secure"] = QVariant(c.isSecure()); + if (c.expirationDate().isValid()) { + cookie["expires"] = QVariant(QString(c.expirationDate().toString("ddd, dd MMM yyyy hh:mm:ss")).append(" GMT")); + cookie["expiry"] = QVariant(c.expirationDate().toMSecsSinceEpoch() / 1000); + } + + result.append(cookie); + } + + return result; +} + +QNetworkCookie CookieJar::cookie(const QString& name, const QString& url) const +{ + QList cookiesList = cookies(url); + for (int i = cookiesList.length() - 1; i >= 0; --i) { + if (cookiesList.at(i).name() == name) { + return cookiesList.at(i); + } + } + return QNetworkCookie(); +} + +QVariantMap CookieJar::cookieToMap(const QString& name, const QString& url) const +{ + QVariantMap cookie; + + QVariantList cookiesList = cookiesToMap(url); + for (int i = cookiesList.length() - 1; i >= 0; --i) { + cookie = cookiesList.at(i).toMap(); + if (cookie["name"].toString() == name) { + return cookie; + } + } + return QVariantMap(); +} + +bool CookieJar::deleteCookie(const QString& name, const QString& url) +{ + bool deleted = false; + if (isEnabled()) { + + // NOTE: This code has been written in an "extended form" to make it + // easy to understand. Surely this could be "shrinked", but it + // would probably look uglier. + + QList cookiesListAll; + + if (url.isEmpty()) { + if (name.isEmpty()) { //< Neither "name" or "url" provided + // This method has been used wrong: + // "redirecting" to the right method for the job + clearCookies(); + } else { //< Only "name" provided + // Delete all cookies with the given name from the CookieJar + cookiesListAll = allCookies(); + for (int i = cookiesListAll.length() - 1; i >= 0; --i) { + if (cookiesListAll.at(i).name() == name) { + // Remove this cookie + qDebug() << "CookieJar - Deleted" << cookiesListAll.at(i).toRawForm(); + cookiesListAll.removeAt(i); + deleted = true; + } + } + } + } else { + // Delete cookie(s) from the ones visible to the given "url". + // Use the "name" to delete only the right one, otherwise all of them. + QList cookiesListUrl = cookies(url); + cookiesListAll = allCookies(); + for (int i = cookiesListAll.length() - 1; i >= 0; --i) { + if (cookiesListUrl.contains(cookiesListAll.at(i)) && //< if it part of the set of cookies visible at URL + (cookiesListAll.at(i).name() == name || name.isEmpty())) { //< and if the name matches, or no name provided + // Remove this cookie + qDebug() << "CookieJar - Deleted" << cookiesListAll.at(i).toRawForm(); + cookiesListAll.removeAt(i); + deleted = true; + + if (!name.isEmpty()) { + // Only one cookie was supposed to be deleted: we are done here! + break; + } + } + } + } + + // Put back the remaining cookies + setAllCookies(cookiesListAll); + } + return deleted; +} + +bool CookieJar::deleteCookies(const QString& url) +{ + if (isEnabled()) { + if (url.isEmpty()) { + // No URL provided: delete ALL the cookies in the CookieJar + clearCookies(); + return true; + } + + // No cookie name provided: delete all the cookies visible by this URL + qDebug() << "Delete all cookies for URL:" << url; + return deleteCookie("", url); + } + return false; +} + +void CookieJar::clearCookies() +{ + if (isEnabled()) { + setAllCookies(QList()); + } +} + +void CookieJar::enable() +{ + m_enabled = true; +} + +void CookieJar::disable() +{ + m_enabled = false; +} + +bool CookieJar::isEnabled() const +{ + return m_enabled; +} + +void CookieJar::close() +{ + deleteLater(); +} + +// private: +bool CookieJar::purgeExpiredCookies() +{ + QList cookiesList = allCookies(); + + // If empty, there is nothing to purge + if (cookiesList.isEmpty()) { + return false; + } + + // Check if any cookie has expired + int prePurgeCookiesCount = cookiesList.count(); + QDateTime now = QDateTime::currentDateTime(); + for (int i = cookiesList.count() - 1; i >= 0; --i) { + if (!cookiesList.at(i).isSessionCookie() && cookiesList.at(i).expirationDate() < now) { + qDebug() << "CookieJar - Purged (expired)" << cookiesList.at(i).toRawForm(); + cookiesList.removeAt(i); + } + } + + // Set cookies and returns "true" if at least 1 cookie expired and has been removed + if (prePurgeCookiesCount != cookiesList.count()) { + setAllCookies(cookiesList); + return true; + } + return false; +} + +bool CookieJar::purgeSessionCookies() +{ + QList cookiesList = allCookies(); + + // If empty, there is nothing to purge + if (cookiesList.isEmpty()) { + return false; + } + + // Check if any cookie has expired + int prePurgeCookiesCount = cookiesList.count(); + for (int i = cookiesList.count() - 1; i >= 0; --i) { + if (cookiesList.at(i).isSessionCookie()) { + qDebug() << "CookieJar - Purged (session)" << cookiesList.at(i).toRawForm(); + cookiesList.removeAt(i); + } + } + + // Set cookies and returns "true" if at least 1 session cookie was found and removed + if (prePurgeCookiesCount != cookiesList.count()) { + setAllCookies(cookiesList); + return true; + } + return false; +} + +void CookieJar::save() +{ + if (isEnabled()) { + // Get rid of all the Cookies that have expired + purgeExpiredCookies(); + +#ifndef QT_NO_DEBUG_OUTPUT + foreach(QNetworkCookie cookie, allCookies()) { + qDebug() << "CookieJar - Saved" << cookie.toRawForm(); + } +#endif + + // Store cookies + if (m_cookieStorage) { + m_cookieStorage->setValue(QLatin1String("cookies"), QVariant::fromValue >(allCookies())); + } + } +} + +void CookieJar::load() +{ + if (isEnabled()) { + // Register a "StreamOperator" for this Meta Type, so we can easily serialize/deserialize the cookies + qRegisterMetaTypeStreamOperators >("QList"); + + // Load all the cookies + if (m_cookieStorage) { + setAllCookies(qvariant_cast >(m_cookieStorage->value(QLatin1String("cookies")))); + } + + // If any cookie has expired since last execution, purge and save before going any further + if (purgeExpiredCookies()) { + save(); + } + +#ifndef QT_NO_DEBUG_OUTPUT + foreach(QNetworkCookie cookie, allCookies()) { + qDebug() << "CookieJar - Loaded" << cookie.toRawForm(); + } +#endif + } +} + +bool CookieJar::contains(const QNetworkCookie& cookieToFind) const +{ + QList cookiesList = allCookies(); + foreach(QNetworkCookie cookie, cookiesList) { + if (cookieToFind == cookie) { + return true; + } + } + + return false; +} diff --git a/src/cookiejar.h b/src/cookiejar.h new file mode 100644 index 0000000..1ced25a --- /dev/null +++ b/src/cookiejar.h @@ -0,0 +1,91 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2012 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef COOKIEJAR_H +#define COOKIEJAR_H + +#include +#include +#include +#include +#include + +class CookieJar: public QNetworkCookieJar +{ + Q_OBJECT + + Q_PROPERTY(QVariantList cookies READ cookiesToMap WRITE addCookiesFromMap) + +public: + CookieJar(QString cookiesFile, QObject* parent = NULL); + virtual ~CookieJar(); + + bool setCookiesFromUrl(const QList& cookieList, const QUrl& url); + QList cookiesForUrl(const QUrl& url) const; + + bool addCookie(const QNetworkCookie& cookie, const QString& url = QString()); + bool addCookies(const QList& cookiesList, const QString& url = QString()); + + QList cookies(const QString& url = QString()) const; + + QNetworkCookie cookie(const QString& name, const QString& url = QString()) const; + + using QNetworkCookieJar::deleteCookie; + bool deleteCookies(const QString& url = QString()); + + void enable(); + void disable(); + bool isEnabled() const; + +public slots: + void addCookie(const QVariantMap& cookie); + bool addCookieFromMap(const QVariantMap& cookie, const QString& url = QString()); + bool addCookiesFromMap(const QVariantList& cookiesList, const QString& url = QString()); + QVariantList cookiesToMap(const QString& url = QString()) const; + QVariantMap cookieToMap(const QString& name, const QString& url = QString()) const; + bool deleteCookie(const QString& name, const QString& url = QString()); + void clearCookies(); + void close(); + +private slots: + bool purgeExpiredCookies(); + bool purgeSessionCookies(); + void save(); + void load(); + +private: + bool contains(const QNetworkCookie& cookie) const; + +private: + QSettings* m_cookieStorage; + bool m_enabled; +}; + +#endif // COOKIEJAR_H diff --git a/src/crashdump.cpp b/src/crashdump.cpp new file mode 100644 index 0000000..b4e259b --- /dev/null +++ b/src/crashdump.cpp @@ -0,0 +1,221 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "crashdump.h" + +#include +#include +#include +#include + +#include + +#if defined(Q_OS_WIN) +#include +#else +#include +#include +#endif + +void +print_crash_message() +{ + fputs("PhantomJS has crashed. Please read the bug reporting guide at\n" + " and file a bug report.\n", + stderr); + fflush(stderr); +} + +#if defined(Q_OS_WIN) + +static LONG WINAPI unhandled_exception_filter(LPEXCEPTION_POINTERS ptrs) +{ + fprintf(stderr, "Fatal Windows exception, code 0x%08x.\n", + ptrs->ExceptionRecord->ExceptionCode); + print_crash_message(); + return EXCEPTION_EXECUTE_HANDLER; +} + +#if _MSC_VER >= 1400 +static void +invalid_parameter_handler(const wchar_t* expression, + const wchar_t* function, + const wchar_t* file, + unsigned int line, + uintptr_t /*reserved*/) +{ + // The parameters all have the value NULL unless a debug version of the CRT library is used + // https://msdn.microsoft.com/en-us/library/a9yf33zb(v=VS.80).aspx +#ifndef _DEBUG + Q_UNUSED(expression); + Q_UNUSED(function); + Q_UNUSED(file); + Q_UNUSED(line); + fprintf(stderr, "Invalid parameter detected.\n"); +#else + fprintf(stderr, "Invalid parameter detected at %ls:%u: %ls: %ls\n", + file, line, function, expression); +#endif // _DEBUG + print_crash_message(); + ExitProcess(STATUS_FATAL_APP_EXIT); +} +#endif + +static void +pure_virtual_call_handler() +{ + fputs("Pure virtual method called.\n", stderr); + print_crash_message(); + ExitProcess(STATUS_FATAL_APP_EXIT); +} + +static void +handle_fatal_signal(int signo) +{ + Q_UNUSED(signo); + + print_crash_message(); + // Because signals on Windows are fake, and because it doesn't provide + // sigaction(), we cannot rely on reraising the exception. + ExitProcess(STATUS_FATAL_APP_EXIT); +} + +static void +init_crash_handler_os() +{ + SetErrorMode(SEM_FAILCRITICALERRORS | + SEM_NOALIGNMENTFAULTEXCEPT | + SEM_NOGPFAULTERRORBOX | + SEM_NOOPENFILEERRORBOX); + SetUnhandledExceptionFilter(unhandled_exception_filter); + + // When the app crashes, don't print the abort message + // and don't call Dr. Watson to make a crash dump. + // http://msdn.microsoft.com/en-us/library/e631wekh(v=VS.100).aspx + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + + +#if _MSC_VER >= 1400 + _set_invalid_parameter_handler(invalid_parameter_handler); +#endif + _set_purecall_handler(pure_virtual_call_handler); + + // Signals on Windows are not operating system primitives and mostly + // shouldn't be used, but installing a handler for SIGABRT is the only + // way to intercept calls to abort(). + signal(SIGABRT, handle_fatal_signal); +} + +#else // not Windows; Unix assumed + +static void +handle_fatal_signal(int signo) +{ + // It would be nice to print the offending signal name here, but + // strsignal() isn't reliably available. Instead we let the shell do it. + print_crash_message(); + raise(signo); +} + +static void +init_crash_handler_os() +{ + const char* offender; + + // Disable core dumps; they are gigantic and useless. + offender = "setrlimit"; + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (setrlimit(RLIMIT_CORE, &rl)) { goto fail; } + + // Ensure that none of the signals that indicate a fatal CPU exception + // are blocked. (If they are delivered while blocked, the behavior is + // undefined per POSIX -- usually the kernel just zaps the process + // without giving it a chance to print a helpful message or anything.) + offender = "sigprocmask"; + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGABRT); + sigaddset(&mask, SIGBUS); + sigaddset(&mask, SIGFPE); + sigaddset(&mask, SIGILL); + sigaddset(&mask, SIGSEGV); + sigaddset(&mask, SIGQUIT); + if (sigprocmask(SIG_UNBLOCK, &mask, 0)) { goto fail; } + + // Install a signal handler for all the above signals. This will call + // print_crash_message and then reraise the signal (so the exit code will + // be accurate). + offender = "sigaction"; + struct sigaction sa; + sigemptyset(&sa.sa_mask); + sa.sa_handler = handle_fatal_signal; + sa.sa_flags = SA_NODEFER | SA_RESETHAND; + + if (sigaction(SIGABRT, &sa, 0)) { goto fail; } + if (sigaction(SIGBUS, &sa, 0)) { goto fail; } + if (sigaction(SIGFPE, &sa, 0)) { goto fail; } + if (sigaction(SIGILL, &sa, 0)) { goto fail; } + if (sigaction(SIGSEGV, &sa, 0)) { goto fail; } + if (sigaction(SIGQUIT, &sa, 0)) { goto fail; } + + return; + +fail: + perror(offender); + exit(1); +} + +#endif // not Windows + +void +init_crash_handler() +{ + // Qt, QtWebkit, and PhantomJS mostly don't make use of C++ exceptions, + // so in the rare cases where an exception does get thrown, it will + // pass all the way up the stack and cause the C++ runtime to call + // std::terminate(). The default std::terminate() handler in some C++ + // runtimes tries to print details of the exception or maybe even a stack + // trace. That's great, but... the most frequent case is bad_alloc, + // thrown because we've run into a system-imposed hard upper limit on + // memory allocation. A clever terminate handler may itself perform more + // memory allocation, which will throw another bad_alloc, and cause a + // recursive call to terminate. In some cases this may happen several + // times before the process finally dies. + // + // So we have last-ditch exception handlers in main.cpp that should catch + // everything, and in case _that_ fails, we replace the terminate handler + // with something that is guaranteed not to allocate memory. + std::set_terminate(abort); + + // Initialize system-specific crash detection. + init_crash_handler_os(); +} diff --git a/src/crashdump.h b/src/crashdump.h new file mode 100644 index 0000000..a2167c4 --- /dev/null +++ b/src/crashdump.h @@ -0,0 +1,37 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef CRASHDUMP_H +#define CRASHDUMP_H + +extern void print_crash_message(); +extern void init_crash_handler(); + +#endif // CRASHDUMP_H diff --git a/src/encoding.cpp b/src/encoding.cpp new file mode 100644 index 0000000..1033d0e --- /dev/null +++ b/src/encoding.cpp @@ -0,0 +1,96 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "encoding.h" + +Encoding::Encoding() +{ + QTextCodec* codec = QTextCodec::codecForName(DEFAULT_CODEC_NAME); + + // Fall back to locale codec + if ((QTextCodec*)NULL == codec) { + codec = QTextCodec::codecForLocale(); + } + + m_codec = codec; +} + +Encoding::Encoding(const QString& encoding) +{ + setEncoding(encoding); +} + +Encoding::~Encoding() +{ + m_codec = (QTextCodec*)NULL; +} + +QString Encoding::decode(const QByteArray& bytes) const +{ + return getCodec()->toUnicode(bytes); +} + +QByteArray Encoding::encode(const QString& string) const +{ + return getCodec()->fromUnicode(string); +} + +QString Encoding::getName() const +{ + // TODO Is it safe to assume UTF-8 here? + return QString::fromUtf8(getCodec()->name()); +} + +void Encoding::setEncoding(const QString& encoding) +{ + if (!encoding.isEmpty()) { + QTextCodec* codec = QTextCodec::codecForName(encoding.toLatin1()); + + if ((QTextCodec*)NULL != codec) { + m_codec = codec; + } + } +} + +const Encoding Encoding::UTF8 = Encoding("UTF-8"); + +// private: +QTextCodec* Encoding::getCodec() const +{ + QTextCodec* codec = m_codec; + + if ((QTextCodec*)NULL == codec) { + codec = QTextCodec::codecForLocale(); + } + + return codec; +} + +const QByteArray Encoding::DEFAULT_CODEC_NAME = "UTF-8"; diff --git a/src/encoding.h b/src/encoding.h new file mode 100644 index 0000000..75022ab --- /dev/null +++ b/src/encoding.h @@ -0,0 +1,58 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ENCODING_H +#define ENCODING_H + +#include + +class Encoding +{ +public: + Encoding(); + Encoding(const QString& encoding); + ~Encoding(); + + QString decode(const QByteArray& bytes) const; + QByteArray encode(const QString& string) const; + + QString getName() const; + void setEncoding(const QString& encoding); + + QTextCodec* getCodec() const; + + static const Encoding UTF8; + +private: + QTextCodec* m_codec; + static const QByteArray DEFAULT_CODEC_NAME; +}; + +#endif // ENCODING_H diff --git a/src/env.cpp b/src/env.cpp new file mode 100644 index 0000000..5e1edda --- /dev/null +++ b/src/env.cpp @@ -0,0 +1,62 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "env.h" + +#include +#include +#include +#include + +static Env* env_instance = NULL; + +Env* Env::instance() +{ + if (NULL == env_instance) { + env_instance = new Env(); + } + + return env_instance; +} + +Env::Env() + : QObject(QCoreApplication::instance()) +{ + const QProcessEnvironment& env = QProcessEnvironment::systemEnvironment(); + foreach(const QString & key, env.keys()) { + m_map[key] = env.value(key); + } +} + +// public: + +QVariantMap Env::asVariantMap() const +{ + return m_map; +} diff --git a/src/env.h b/src/env.h new file mode 100644 index 0000000..e836625 --- /dev/null +++ b/src/env.h @@ -0,0 +1,51 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ENV_H +#define ENV_H + +#include +#include + +class Env : public QObject +{ + Q_OBJECT + +public: + static Env* instance(); + + QVariantMap asVariantMap() const; + +private: + Env(); + + QVariantMap m_map; +}; + +#endif // ENV_H diff --git a/src/filesystem.cpp b/src/filesystem.cpp new file mode 100644 index 0000000..9160d8a --- /dev/null +++ b/src/filesystem.cpp @@ -0,0 +1,538 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "filesystem.h" + +#include +#include +#include +#include +#include + +// File +// public: +File::File(QFile* openfile, QTextCodec* codec, QObject* parent) : + QObject(parent), + m_file(openfile), + m_fileStream(0) +{ + if (codec) { + m_fileStream = new QTextStream(m_file); + m_fileStream->setCodec(codec); + } +} + +File::~File() +{ + this->close(); +} + +//NOTE: for binary files we want to use QString instead of QByteArray as the +// latter is not really useable in javascript and e.g. window.btoa expects a string +// and we need special code required since fromAsci() would stop as soon as it +// encounters \0 or similar + +// public slots: +QString File::read(const QVariant& n) +{ + // Default to 1024 (used when n is "null") + qint64 bytesToRead = 1024; + + // If parameter can be converted to a qint64, do so and use that value instead + if (n.canConvert(QVariant::LongLong)) { + bytesToRead = n.toLongLong(); + } + + const bool isReadAll = 0 > bytesToRead; + + if (!m_file->isReadable()) { + qDebug() << "File::read - " << "Couldn't read:" << m_file->fileName(); + return QString(); + } + if (m_file->isWritable()) { + // make sure we write everything to disk before reading + flush(); + } + if (m_fileStream) { + // text file + QString ret; + if (isReadAll) { + // This code, for some reason, reads the whole file from 0 to EOF, + // and then resets to the position the file was at prior to reading + const qint64 pos = m_fileStream->pos(); + m_fileStream->seek(0); + ret = m_fileStream->readAll(); + m_fileStream->seek(pos); + } else { + ret = m_fileStream->read(bytesToRead); + } + return ret; + } else { + // binary file + QByteArray data; + if (isReadAll) { + // This code, for some reason, reads the whole file from 0 to EOF, + // and then resets to the position the file was at prior to reading + const qint64 pos = m_file->pos(); + m_file->seek(0); + data = m_file->readAll(); + m_file->seek(pos); + } else { + data = m_file->read(bytesToRead); + } + QString ret(data.size(), ' '); + for (int i = 0; i < data.size(); ++i) { + ret[i] = data.at(i); + } + return ret; + } +} + +bool File::write(const QString& data) +{ + if (!m_file->isWritable()) { + qDebug() << "File::write - " << "Couldn't write:" << m_file->fileName(); + return true; + } + if (m_fileStream) { + // text file + (*m_fileStream) << data; + if (_isUnbuffered()) { + m_fileStream->flush(); + } + return true; + } else { + // binary file + QByteArray bytes(data.size(), Qt::Uninitialized); + for (int i = 0; i < data.size(); ++i) { + bytes[i] = data.at(i).toLatin1(); + } + return m_file->write(bytes); + } +} + +bool File::seek(const qint64 pos) +{ + if (m_fileStream) { + return m_fileStream->seek(pos); + } else { + return m_file->seek(pos); + } +} + +QString File::readLine() +{ + if (!m_file->isReadable()) { + qDebug() << "File::readLine - " << "Couldn't read:" << m_file->fileName(); + return QString(); + } + if (m_file->isWritable()) { + // make sure we write everything to disk before reading + flush(); + } + if (m_fileStream) { + // text file + return m_fileStream->readLine(); + } else { + // binary file - doesn't make much sense but well... + return QString::fromLatin1(m_file->readLine()); + } +} + +bool File::writeLine(const QString& data) +{ + if (write(data) && write("\n")) { + return true; + } + qDebug() << "File::writeLine - " << "Couldn't write:" << m_file->fileName(); + return false; +} + +bool File::atEnd() const +{ + if (m_file->isReadable()) { + if (m_fileStream) { + // text file + return m_fileStream->atEnd(); + } else { + // binary file + return m_file->atEnd(); + } + } + qDebug() << "File::atEnd - " << "Couldn't read:" << m_file->fileName(); + return false; +} + +void File::flush() +{ + if (m_file) { + if (m_fileStream) { + // text file + m_fileStream->flush(); + } + // binary or text file + m_file->flush(); + } +} + +void File::close() +{ + flush(); + if (m_fileStream) { + delete m_fileStream; + m_fileStream = 0; + } + if (m_file) { + m_file->close(); + delete m_file; + m_file = NULL; + } + deleteLater(); +} + +bool File::setEncoding(const QString& encoding) +{ + if (encoding.isEmpty() || encoding.isNull()) { + return false; + } + + // "Binary" mode doesn't use/need text codecs + if ((QTextStream*)NULL == m_fileStream) { + // TODO: Should we switch to "text" mode? + return false; + } + + // Since there can be multiple names for the same codec (i.e., "utf8" and + // "utf-8"), we need to get the codec in the system first and use its + // canonical name + QTextCodec* codec = QTextCodec::codecForName(encoding.toLatin1()); + if ((QTextCodec*)NULL == codec) { + return false; + } + + // Check whether encoding actually needs to be changed + const QString encodingBeforeUpdate(m_fileStream->codec()->name()); + if (0 == encodingBeforeUpdate.compare(QString(codec->name()), Qt::CaseInsensitive)) { + return false; + } + + m_fileStream->setCodec(codec); + + // Return whether update was successful + const QString encodingAfterUpdate(m_fileStream->codec()->name()); + return 0 != encodingBeforeUpdate.compare(encodingAfterUpdate, Qt::CaseInsensitive); +} + +QString File::getEncoding() const +{ + QString encoding; + + if ((QTextStream*)NULL != m_fileStream) { + encoding = QString(m_fileStream->codec()->name()); + } + + return encoding; +} + +// private: + +bool File::_isUnbuffered() const +{ + return m_file->openMode() & QIODevice::Unbuffered; +} + + +// FileSystem +// public: +FileSystem::FileSystem(QObject* parent) + : QObject(parent) +{ } + +// public slots: + +// Attributes +int FileSystem::_size(const QString& path) const +{ + QFileInfo fi(path); + if (fi.exists()) { + return fi.size(); + } + return -1; +} + +QVariant FileSystem::lastModified(const QString& path) const +{ + QFileInfo fi(path); + if (fi.exists()) { + return QVariant(fi.lastModified()); + } + return QVariant(QDateTime()); +} + +// Links +QString FileSystem::readLink(const QString& path) const +{ + return QFileInfo(path).symLinkTarget(); +} + +// Tests +bool FileSystem::exists(const QString& path) const +{ + return QFile::exists(path); +} + +bool FileSystem::isDirectory(const QString& path) const +{ + return QFileInfo(path).isDir(); +} + +bool FileSystem::isFile(const QString& path) const +{ + return QFileInfo(path).isFile(); +} + +bool FileSystem::isAbsolute(const QString& path) const +{ + return QFileInfo(path).isAbsolute(); +} + +bool FileSystem::isExecutable(const QString& path) const +{ + return QFileInfo(path).isExecutable(); +} + +bool FileSystem::isLink(const QString& path) const +{ + return QFileInfo(path).isSymLink(); +} + +bool FileSystem::isReadable(const QString& path) const +{ + return QFileInfo(path).isReadable(); +} + +bool FileSystem::isWritable(const QString& path) const +{ + return QFileInfo(path).isWritable(); +} + +// Directory +bool FileSystem::_copyTree(const QString& source, const QString& destination) const +{ + QDir sourceDir(source); + QDir::Filters sourceDirFilter = QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Files | QDir::NoSymLinks | QDir::Drives; + + if (sourceDir.exists()) { + // Make the destination directory if it doesn't exist already + if (!FileSystem::exists(destination) && !FileSystem::makeDirectory(destination)) { + return false; + } + + foreach(QFileInfo entry, sourceDir.entryInfoList(sourceDirFilter, QDir::DirsFirst)) { + if (entry.isDir()) { + if (!FileSystem::_copyTree(entry.absoluteFilePath(), + destination + "/" + entry.fileName())) { //< directory: recursive call + return false; + } + } else { + if (!FileSystem::_copy(entry.absoluteFilePath(), + destination + "/" + entry.fileName())) { //< file: copy + return false; + } + } + } + } + + return true; +} + +bool FileSystem::makeDirectory(const QString& path) const +{ + return QDir().mkdir(path); +} + +bool FileSystem::makeTree(const QString& path) const +{ + return QDir().mkpath(path); +} + +bool FileSystem::_removeDirectory(const QString& path) const +{ + return QDir().rmdir(path); +} + +bool FileSystem::_removeTree(const QString& path) const +{ + QDir dir(path); + QDir::Filters dirFilter = QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files; + + if (dir.exists()) { + foreach(QFileInfo info, dir.entryInfoList(dirFilter, QDir::DirsFirst)) { + if (info.isDir()) { + if (!FileSystem::_removeTree(info.absoluteFilePath())) { //< directory: recursive call + return false; + } + } else { + if (!FileSystem::_remove(info.absoluteFilePath())) { //< file: remove + return false; + } + } + } + if (!FileSystem::_removeDirectory(path)) { //< delete the top tree directory + return false; + } + } + + return true; +} + +QStringList FileSystem::list(const QString& path) const +{ + return QDir(path).entryList(); +} + +// Paths +QString FileSystem::separator() const +{ + return QDir::separator(); +} + +QString FileSystem::workingDirectory() const +{ + return QDir::currentPath(); +} + +bool FileSystem::changeWorkingDirectory(const QString& path) const +{ + return QDir::setCurrent(path); +} + +QString FileSystem::absolute(const QString& relativePath) const +{ + return QFileInfo(relativePath).absoluteFilePath(); +} + +QString FileSystem::fromNativeSeparators(const QString& path) const +{ + return QDir::fromNativeSeparators(path); +} + +QString FileSystem::toNativeSeparators(const QString& path) const +{ + return QDir::toNativeSeparators(path); +} + +// Files +QObject* FileSystem::_open(const QString& path, const QVariantMap& opts) const +{ + qDebug() << "FileSystem - _open:" << path << opts; + + const QVariant modeVar = opts["mode"]; + // Ensure only strings + if (modeVar.type() != QVariant::String) { + qDebug() << "FileSystem::open - " << "Mode must be a string!" << modeVar; + return 0; + } + + bool isBinary = false; + QFile::OpenMode modeCode = QFile::NotOpen; + + // Determine the OpenMode + foreach(const QChar & c, modeVar.toString()) { + switch (c.toLatin1()) { + case 'r': case 'R': { + modeCode |= QFile::ReadOnly; + break; + } + case 'a': case 'A': case '+': { + modeCode |= QFile::Append; + modeCode |= QFile::WriteOnly; + break; + } + case 'w': case 'W': { + modeCode |= QFile::WriteOnly; + break; + } + case 'b': case 'B': { + isBinary = true; + break; + } + default: { + qDebug() << "FileSystem::open - " << "Wrong Mode:" << c; + return 0; + } + } + } + + // Make sure the file exists OR it can be created at the required path + if (!QFile::exists(path) && modeCode & QFile::WriteOnly) { + if (!makeTree(QFileInfo(path).dir().absolutePath())) { + qDebug() << "FileSystem::open - " << "Full path coulnd't be created:" << path; + return 0; + } + } + + // Make sure there is something to read + if (!QFile::exists(path) && modeCode & QFile::ReadOnly) { + qDebug() << "FileSystem::open - " << "Trying to read a file that doesn't exist:" << path; + return 0; + } + + QTextCodec* codec = 0; + if (!isBinary) { + // default to UTF-8 encoded files + const QString charset = opts.value("charset", "UTF-8").toString(); + codec = QTextCodec::codecForName(charset.toLatin1()); + if (!codec) { + qDebug() << "FileSystem::open - " << "Unknown charset:" << charset; + return 0; + } + } + + // Try to Open + QFile* file = new QFile(path); + if (!file->open(modeCode)) { + // Return "NULL" if the file couldn't be opened as requested + delete file; + qDebug() << "FileSystem::open - " << "Couldn't be opened:" << path; + return 0; + } + + return new File(file, codec); +} + +bool FileSystem::_remove(const QString& path) const +{ + return QFile::remove(path); +} + +bool FileSystem::_copy(const QString& source, const QString& destination) const +{ + return QFile(source).copy(destination); +} diff --git a/src/filesystem.h b/src/filesystem.h new file mode 100644 index 0000000..7a7c680 --- /dev/null +++ b/src/filesystem.h @@ -0,0 +1,144 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ivan De Marino + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef FILESYSTEM_H +#define FILESYSTEM_H + +#include +#include +#include +#include +#include + +class File : public QObject +{ + Q_OBJECT + +public: + // handle a textfile with given codec + // if @p codec is null, the file is considered to be binary + File(QFile* openfile, QTextCodec* codec, QObject* parent = 0); + virtual ~File(); + +public slots: + /** + * @param n Number of bytes to read (a negative value means read up to EOF) + * NOTE: The use of QVariant here is necessary to catch JavaScript `null`. + * @see IO/A spec + */ + QString read(const QVariant& n = -1); + bool write(const QString& data); + + bool seek(const qint64 pos); + + QString readLine(); + bool writeLine(const QString& data); + + bool atEnd() const; + void flush(); + void close(); + + QString getEncoding() const; + bool setEncoding(const QString& encoding); + +private: + bool _isUnbuffered() const; + + QFile* m_file; + QTextStream* m_fileStream; +}; + + +class FileSystem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString workingDirectory READ workingDirectory) + Q_PROPERTY(QString separator READ separator) + +public: + FileSystem(QObject* parent = 0); + +public slots: + // Attributes + // 'size(path)' implemented in "filesystem-shim.js" using '_size(path)' + int _size(const QString& path) const; + QVariant lastModified(const QString& path) const; + + // Directory + // 'copyTree(source, destination)' implemented in "filesystem-shim.js" using '_copyTree(source, destination)' + bool _copyTree(const QString& source, const QString& destination) const; + bool makeDirectory(const QString& path) const; + bool makeTree(const QString& path) const; + // 'removeDirectory(path)' implemented in "filesystem-shim.js" using '_removeDirectory(path)' + bool _removeDirectory(const QString& path) const; + // 'removeTree(path)' implemented in "filesystem-shim.js" using '_removeTree(path)' + bool _removeTree(const QString& path) const; + + // Files + // 'open(path, mode|options)' implemented in "filesystem-shim.js" using '_open(path, opts)' + QObject* _open(const QString& path, const QVariantMap& opts) const; + // 'read(path, options)' implemented in "filesystem-shim.js" + // 'readRaw(path, options)' implemented in "filesystem-shim.js" + // 'write(path, mode|options)' implemented in the "filesystem-shim.js" + // 'writeRaw(path, mode|options)' implemented in the "filesystem-shim.js" + // 'remove(path)' implemented in "filesystem-shim.js" using '_remove(path)' + bool _remove(const QString& path) const; + // 'copy(source, destination)' implemented in "filesystem-shim.js" using '_copy(source, destination)' + bool _copy(const QString& source, const QString& destination) const; + // 'move(source, destination)' implemented in "filesystem-shim.js" + // 'touch(path)' implemented in "filesystem-shim.js" + + // Listing + QStringList list(const QString& path) const; + + // Paths + QString separator() const; + QString workingDirectory() const; + bool changeWorkingDirectory(const QString& path) const; + QString absolute(const QString& relativePath) const; + // 'join(...)' implemented in "fs.js" + // 'split(path)' implemented in "fs.js" + QString fromNativeSeparators(const QString& path) const; + QString toNativeSeparators(const QString& path) const; + + // Links + QString readLink(const QString& path) const; + + // Tests + bool exists(const QString& path) const; + bool isDirectory(const QString& path) const; + bool isFile(const QString& path) const; + bool isAbsolute(const QString& path) const; + bool isExecutable(const QString& path) const; + bool isReadable(const QString& path) const; + bool isWritable(const QString& path) const; + bool isLink(const QString& path) const; +}; + +#endif // FILESYSTEM_H diff --git a/src/ghostdriver/README.md b/src/ghostdriver/README.md new file mode 100644 index 0000000..56b44b0 --- /dev/null +++ b/src/ghostdriver/README.md @@ -0,0 +1,7 @@ +# PLEASE DON'T CHANGE THIS FILE +This file is auto-generated by export scripts **from GhostDriver to PhantomJS**. +If you want to make changes to GhostDriver source, +please refer to that project instead: `https://github.com/detro/ghostdriver`. + +Thanks, +[Ivan De Marino](http://ivandemarino.me) diff --git a/src/ghostdriver/config.js b/src/ghostdriver/config.js new file mode 100644 index 0000000..985bd62 --- /dev/null +++ b/src/ghostdriver/config.js @@ -0,0 +1,104 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Default configuration +var defaultConfig = { + "ip" : "127.0.0.1", + "port" : "8910", + "hub" : null, + "proxy" : "org.openqa.grid.selenium.proxy.DefaultRemoteProxy", + "version" : "", + "logFile" : null, + "logLevel" : "INFO", + "logColor" : false + }, + config = { + "ip" : defaultConfig.ip, + "port" : defaultConfig.port, + "hub" : defaultConfig.hub, + "proxy" : defaultConfig.proxy, + "version" : defaultConfig.version, + "logFile" : defaultConfig.logFile, + "logLevel" : defaultConfig.logLevel, + "logColor" : defaultConfig.logColor + }, + logOutputFile = null, + logger = require("./logger.js"), + _log = logger.create("Config"); + +function apply () { + // Normalise and Set Console Logging Level + config.logLevel = config.logLevel.toUpperCase(); + if (!logger.console.LEVELS.hasOwnProperty(config.logLevel)) { + config.logLevel = defaultConfig.logLevel; + } + logger.console.setLevel(logger.console.LEVELS[config.logLevel]); + + // Normalise and Set Console Color + try { + config.logColor = JSON.parse(config.logColor); + } catch (e) { + config.logColor = defaultConfig.logColor; + } + if (config.logColor) { + logger.console.enableColor(); + } else { + logger.console.disableColor(); + } + + // Add a Log File (if any) + if (config.logFile !== null) { + logger.addLogFile(config.logFile); + } +} + +exports.init = function(cliArgs) { + var i, k, + regexp = new RegExp("^--([a-z]+)=([a-z0-9_/\\\\:.]+)$", "i"), + regexpRes; + + // Loop over all the Command Line Arguments + // If any of the form '--param=value' is found, it's compared against + // the 'config' object to see if 'config.param' exists. + for (i = cliArgs.length -1; i >= 1; --i) { + // Apply Regular Expression + regexpRes = regexp.exec(cliArgs[i]); + if (regexpRes !== null && regexpRes.length === 3 && + config.hasOwnProperty(regexpRes[1])) { + config[regexpRes[1]] = regexpRes[2]; + } + } + + // Apply/Normalize the Configuration before returning + apply(); + + _log.debug("config.init", JSON.stringify(config)); +}; + +exports.get = function() { + return config; +}; diff --git a/src/ghostdriver/errors.js b/src/ghostdriver/errors.js new file mode 100644 index 0000000..225f64f --- /dev/null +++ b/src/ghostdriver/errors.js @@ -0,0 +1,235 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +//------------------------------------------------------- Invalid Request Errors +//----- http://code.google.com/p/selenium/wiki/JsonWireProtocol#Invalid_Requests +exports.INVALID_REQ = { + "UNKNOWN_COMMAND" : "Unknown Command", + "UNIMPLEMENTED_COMMAND" : "Unimplemented Command", + "VARIABLE_RESOURCE_NOT_FOUND" : "Variable Resource Not Found", + "INVALID_COMMAND_METHOD" : "Invalid Command Method", + "MISSING_COMMAND_PARAMETER" : "Missing Command Parameter" +}; + +var _invalidReqHandle = function(res) { + // Set the right Status Code + switch(this.name) { + case exports.INVALID_REQ.UNIMPLEMENTED_COMMAND: + res.statusCode = 501; //< 501 Not Implemented + break; + case exports.INVALID_REQ.INVALID_COMMAND_METHOD: + res.statusCode = 405; //< 405 Method Not Allowed + break; + case exports.INVALID_REQ.MISSING_COMMAND_PARAMETER: + res.statusCode = 400; //< 400 Bad Request + break; + default: + res.statusCode = 404; //< 404 Not Found + break; + } + + res.setHeader("Content-Type", "text/plain"); + res.writeAndClose(this.name + " - " + this.message); +}; + +// Invalid Request Error Handler +exports.createInvalidReqEH = function(errorName, req) { + var e = new Error(); + + e.name = errorName; + e.message = JSON.stringify(req); + e.handle = _invalidReqHandle; + + return e; +}; +exports.handleInvalidReqEH = function(errorName, req, res) { + exports.createInvalidReqEH(errorName, req).handle(res); +}; + +// Invalid Request Unknown Command Error Handler +exports.createInvalidReqUnknownCommandEH = function(req) { + return exports.createInvalidReqEH ( + exports.INVALID_REQ.UNKNOWN_COMMAND, + req); +}; +exports.handleInvalidReqUnknownCommandEH = function(req, res) { + exports.createInvalidReqUnknownCommandEH(req).handle(res); +}; + +// Invalid Request Unimplemented Command Error Handler +exports.createInvalidReqUnimplementedCommandEH = function(req) { + return exports.createInvalidReqEH ( + exports.INVALID_REQ.UNIMPLEMENTED_COMMAND, + req); +}; +exports.handleInvalidReqUnimplementedCommandEH = function(req, res) { + exports.createInvalidReqUnimplementedCommandEH(req).handle(res); +}; + +// Invalid Request Variable Resource Not Found Error Handler +exports.createInvalidReqVariableResourceNotFoundEH = function(req) { + return exports.createInvalidReqEH ( + exports.INVALID_REQ.VARIABLE_RESOURCE_NOT_FOUND, + req); +}; +exports.handleInvalidReqVariableResourceNotFoundEH = function(req, res) { + exports.createInvalidReqVariableResourceNotFoundEH(req).handle(res); +}; + +// Invalid Request Invalid Command Method Error Handler +exports.createInvalidReqInvalidCommandMethodEH = function(req) { + return exports.createInvalidReqEH ( + exports.INVALID_REQ.INVALID_COMMAND_METHOD, + req); +}; +exports.handleInvalidReqInvalidCommandMethodEH = function(req, res) { + exports.createInvalidReqInvalidCommandMethodEH(req).handle(res); +}; + +// Invalid Request Missing Command Parameter Error Handler +exports.createInvalidReqMissingCommandParameterEH = function(req) { + return exports.createInvalidReqEH ( + exports.INVALID_REQ.MISSING_COMMAND_PARAMETER, + req); +}; +exports.handleInvalidReqMissingCommandParameterEH = function(req, res) { + exports.createInvalidReqMissingCommandParameterEH(req).handle(res); +}; + +//-------------------------------------------------------- Failed Command Errors +//------ http://code.google.com/p/selenium/wiki/JsonWireProtocol#Failed_Commands + +// Possible Failed Status Codes +exports.FAILED_CMD_STATUS_CODES = { + "Success" : 0, + "NoSuchElement" : 7, + "NoSuchFrame" : 8, + "UnknownCommand" : 9, + "StaleElementReference" : 10, + "ElementNotVisible" : 11, + "InvalidElementState" : 12, + "UnknownError" : 13, + "ElementIsNotSelectable" : 15, + "JavaScriptError" : 17, + "XPathLookupError" : 19, + "Timeout" : 21, + "NoSuchWindow" : 23, + "InvalidCookieDomain" : 24, + "UnableToSetCookie" : 25, + "UnexpectedAlertOpen" : 26, + "NoAlertOpenError" : 27, + "ScriptTimeout" : 28, + "InvalidElementCoordinates" : 29, + "IMENotAvailable" : 30, + "IMEEngineActivationFailed" : 31, + "InvalidSelector" : 32 +}; + +// Possible Failed Status Code Names +exports.FAILED_CMD_STATUS_CODES_NAMES = []; +exports.FAILED_CMD_STATUS_CODES_NAMES[0] = "Success"; +exports.FAILED_CMD_STATUS_CODES_NAMES[7] = "NoSuchElement"; +exports.FAILED_CMD_STATUS_CODES_NAMES[8] = "NoSuchFrame"; +exports.FAILED_CMD_STATUS_CODES_NAMES[9] = "UnknownCommand"; +exports.FAILED_CMD_STATUS_CODES_NAMES[10] = "StaleElementReference"; +exports.FAILED_CMD_STATUS_CODES_NAMES[11] = "ElementNotVisible"; +exports.FAILED_CMD_STATUS_CODES_NAMES[12] = "InvalidElementState"; +exports.FAILED_CMD_STATUS_CODES_NAMES[13] = "UnknownError"; +exports.FAILED_CMD_STATUS_CODES_NAMES[15] = "ElementIsNotSelectable"; +exports.FAILED_CMD_STATUS_CODES_NAMES[17] = "JavaScriptError"; +exports.FAILED_CMD_STATUS_CODES_NAMES[19] = "XPathLookupError"; +exports.FAILED_CMD_STATUS_CODES_NAMES[21] = "Timeout"; +exports.FAILED_CMD_STATUS_CODES_NAMES[23] = "NoSuchWindow"; +exports.FAILED_CMD_STATUS_CODES_NAMES[24] = "InvalidCookieDomain"; +exports.FAILED_CMD_STATUS_CODES_NAMES[25] = "UnableToSetCookie"; +exports.FAILED_CMD_STATUS_CODES_NAMES[26] = "UnexpectedAlertOpen"; +exports.FAILED_CMD_STATUS_CODES_NAMES[27] = "NoAlertOpenError"; +exports.FAILED_CMD_STATUS_CODES_NAMES[28] = "ScriptTimeout"; +exports.FAILED_CMD_STATUS_CODES_NAMES[29] = "InvalidElementCoordinates"; +exports.FAILED_CMD_STATUS_CODES_NAMES[30] = "IMENotAvailable"; +exports.FAILED_CMD_STATUS_CODES_NAMES[31] = "IMEEngineActivationFailed"; +exports.FAILED_CMD_STATUS_CODES_NAMES[32] = "InvalidSelector"; + +// Possible Failed Status Classnames +exports.FAILED_CMD_STATUS_CLASSNAMES = []; +exports.FAILED_CMD_STATUS_CLASSNAMES[7] = "org.openqa.selenium.NoSuchElementException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[8] = "org.openqa.selenium.NoSuchFrameException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[9] = "org.openqa.selenium.UnsupportedCommandException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[10] = "org.openqa.selenium.StaleElementReferenceException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[11] = "org.openqa.selenium.ElementNotVisibleException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[12] = "org.openqa.selenium.InvalidElementStateException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[13] = "org.openqa.selenium.WebDriverException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[15] = "org.openqa.selenium.InvalidSelectorException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[17] = "org.openqa.selenium.WebDriverException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[19] = "org.openqa.selenium.InvalidSelectorException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[21] = "org.openqa.selenium.TimeoutException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[23] = "org.openqa.selenium.NoSuchWindowException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[24] = "org.openqa.selenium.InvalidCookieDomainException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[25] = "org.openqa.selenium.UnableToSetCookieException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[26] = "org.openqa.selenium.UnhandledAlertException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[27] = "org.openqa.selenium.NoAlertPresentException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[28] = "org.openqa.selenium.WebDriverException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[29] = "org.openqa.selenium.interactions.InvalidCoordinatesException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[30] = "org.openqa.selenium.ImeNotAvailableException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[31] = "org.openqa.selenium.ImeActivationFailedException"; +exports.FAILED_CMD_STATUS_CLASSNAMES[32] = "org.openqa.selenium.InvalidSelectorException"; + +var _failedCommandHandle = function(res) { + // Generate response body + var body = { + "sessionId" : this.errorSessionId, + "status" : this.errorStatusCode, + "value" : { + "message" : this.message, + "screen" : this.errorScreenshot, + "class" : this.errorClassName + } + }; + + // Send it + res.statusCode = 500; //< 500 Internal Server Error + res.writeJSONAndClose(body); +}; + +// Failed Command Error Handler +exports.createFailedCommandEH = function (errorCode, errorMsg, req, session) { + var e = new Error(); + + e.errorStatusCode = isNaN(errorCode) ? exports.FAILED_CMD_STATUS_CODES.UnknownError : errorCode; + e.name = exports.FAILED_CMD_STATUS_CODES_NAMES[e.errorStatusCode]; + e.message = JSON.stringify({ "errorMessage" : errorMsg, "request" : req }); + e.errorSessionId = session.getId() || null; + e.errorClassName = exports.FAILED_CMD_STATUS_CLASSNAMES[e.errorStatusCode] || "unknown"; + e.errorScreenshot = (session.getCapabilities().takesScreenshot && session.getCurrentWindow() !== null) ? + session.getCurrentWindow().renderBase64("png") : ""; + e.handle = _failedCommandHandle; + + return e; +}; +exports.handleFailedCommandEH = function (errorCode, errorMsg, req, res, session) { + exports.createFailedCommandEH(errorCode, errorMsg, req, session).handle(res); +}; diff --git a/src/ghostdriver/ghostdriver.qrc b/src/ghostdriver/ghostdriver.qrc new file mode 100644 index 0000000..e782ba3 --- /dev/null +++ b/src/ghostdriver/ghostdriver.qrc @@ -0,0 +1,24 @@ + + + config.js + errors.js + hub_register.js + inputs.js + logger.js + main.js + request_handlers/request_handler.js + request_handlers/router_request_handler.js + request_handlers/session_manager_request_handler.js + request_handlers/session_request_handler.js + request_handlers/shutdown_request_handler.js + request_handlers/status_request_handler.js + request_handlers/webelement_request_handler.js + session.js + third_party/console++.js + third_party/har.js + third_party/parseuri.js + third_party/uuid.js + webdriver_atoms.js + webelementlocator.js + + diff --git a/src/ghostdriver/hub_register.js b/src/ghostdriver/hub_register.js new file mode 100644 index 0000000..bae3a48 --- /dev/null +++ b/src/ghostdriver/hub_register.js @@ -0,0 +1,99 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* generate node configuration for this node */ +var nodeconf = function(ip, port, hub, proxy, version) { + var ref$, hubHost, hubPort; + + ref$ = hub.match(/([\w\d\.]+):(\d+)/); + hubHost = ref$[1]; + hubPort = +ref$[2]; //< ensure it's of type "number" + + var ghostdriver = ghostdriver || {}; + + return { + capabilities: [{ + browserName: "phantomjs", + version: version, + platform: ghostdriver.system.os.name + '-' + ghostdriver.system.os.version + '-' + ghostdriver.system.os.architecture, + maxInstances: 1, + seleniumProtocol: "WebDriver" + }], + configuration: { + hub: hub, + hubHost: hubHost, + hubPort: hubPort, + host: ip, + port: port, + proxy: proxy, + // Note that multiple webdriver sessions or instances within a single + // Ghostdriver process will interact in unexpected and undesirable ways. + maxSession: 1, + register: true, + registerCycle: 5000, + role: "wd", + url: "http://" + ip + ":" + port, + remoteHost: "http://" + ip + ":" + port + } + }; + }, + _log = require("./logger.js").create("HUB Register"); + +module.exports = { + register: function(ip, port, hub, proxy, version) { + var page; + + try { + page = require('webpage').create(); + port = +port; //< ensure it's of type "number" + if(!hub.match(/\/$/)) { + hub += '/'; + } + + /* Register with selenium grid server */ + page.open(hub + 'grid/register', { + operation: 'post', + data: JSON.stringify(nodeconf(ip, port, hub, proxy, version)), + headers: { + 'Content-Type': 'application/json' + } + }, function(status) { + if(status !== 'success') { + _log.error("register", "Unable to contact grid " + hub + ": " + status); + phantom.exit(1); + } + if(page.framePlainText !== "ok") { + _log.error("register", "Problem registering with grid " + hub + ": " + page.content); + phantom.exit(1); + } + _log.info("register", "Registered with grid hub: " + hub + " (" + page.framePlainText + ")"); + }); + } catch (e) { + throw new Error("Could not register to Selenium Grid Hub: " + hub); + } + } +}; diff --git a/src/ghostdriver/inputs.js b/src/ghostdriver/inputs.js new file mode 100644 index 0000000..0c10708 --- /dev/null +++ b/src/ghostdriver/inputs.js @@ -0,0 +1,346 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Jim Evans - Salesforce.com +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.Inputs = function () { + // private: + const + _specialKeys = { + '\uE000': "Escape", // NULL + '\uE001': "Cancel", // Cancel + '\uE002': "F1", // Help + '\uE003': "Backspace", // Backspace + '\uE004': "Tab", // Tab + '\uE005': "Clear", // Clear + '\uE006': "\n", + '\uE007': "Enter", + '\uE008': "Shift", // Shift + '\uE009': "Control", // Control + '\uE00A': "Alt", // Alt + '\uE00B': "Pause", // Pause + '\uE00C': "Escape", // Escape + '\uE00D': "Space", // Space + '\uE00E': "PageUp", // PageUp + '\uE00F': "PageDown", // PageDown + '\uE010': "End", // End + '\uE011': "Home", // Home + '\uE012': "Left", // Left arrow + '\uE013': "Up", // Up arrow + '\uE014': "Right", // Right arrow + '\uE015': "Down", // Down arrow + '\uE016': "Insert", // Insert + '\uE017': "Delete", // Delete + '\uE018': ";", // Semicolon + '\uE019': "=", // Equals + '\uE01A': "0", // Numpad 0 + '\uE01B': "1", // Numpad 1 + '\uE01C': "2", // Numpad 2 + '\uE01D': "3", // Numpad 3 + '\uE01E': "4", // Numpad 4 + '\uE01F': "5", // Numpad 5 + '\uE020': "6", // Numpad 6 + '\uE021': "7", // Numpad 7 + '\uE022': "8", // Numpad 8 + '\uE023': "9", // Numpad 9 + '\uE024': "*", // Multiply + '\uE025': "+", // Add + '\uE026': ",", // Separator + '\uE027': "-", // Subtract + '\uE028': ".", // Decimal + '\uE029': "/", // Divide + '\uE031': "F1", // F1 + '\uE032': "F2", // F2 + '\uE033': "F3", // F3 + '\uE034': "F4", // F4 + '\uE035': "F5", // F5 + '\uE036': "F6", // F6 + '\uE037': "F7", // F7 + '\uE038': "F8", // F8 + '\uE039': "F9", // F9 + '\uE03A': "F10", // F10 + '\uE03B': "F11", // F11 + '\uE03C': "F12", // F12 + '\uE03D': "Meta" // Command/Meta + }, + + _implicitShiftKeys = { + "A": "a", + "B": "b", + "C": "c", + "D": "d", + "E": "e", + "F": "f", + "G": "g", + "H": "h", + "I": "i", + "J": "j", + "K": "k", + "L": "l", + "M": "m", + "N": "n", + "O": "o", + "P": "p", + "Q": "q", + "R": "r", + "S": "s", + "T": "t", + "U": "u", + "V": "v", + "W": "w", + "X": "x", + "Y": "y", + "Z": "z", + "!": "1", + "@": "2", + "#": "3", + "$": "4", + "%": "5", + "^": "6", + "&": "7", + "*": "8", + "(": "9", + ")": "0", + "_": "-", + "+": "=", + "{": "[", + "}": "]", + "|": "\\", + ":": ";", + "<": ",", + ">": ".", + "?": "/", + "~": "`", + "\"": "'" + }, + + _shiftKeys = { + "a": "A", + "b": "B", + "c": "C", + "d": "D", + "e": "E", + "f": "F", + "g": "G", + "h": "H", + "i": "I", + "j": "J", + "k": "K", + "l": "L", + "m": "M", + "n": "N", + "o": "O", + "p": "P", + "q": "Q", + "r": "R", + "s": "S", + "t": "T", + "u": "U", + "v": "V", + "w": "W", + "x": "X", + "y": "Y", + "z": "Z", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + "\\": "|", + ";": ":", + ",": "<", + ".": ">", + "/": "?", + "`": "~", + "'": "\"" + }, + + _modifierKeyValues = { + "SHIFT": 0x02000000, // A Shift key on the keyboard is pressed. + "CONTROL": 0x04000000, // A Ctrl key on the keyboard is pressed. + "ALT": 0x08000000, // An Alt key on the keyboard is pressed. + "META": 0x10000000, // A Meta key on the keyboard is pressed. + "NUMPAD": 0x20000000 // Keypad key. + }; + + var + _mousePos = { x: 0, y: 0 }, + _keyboardState = {}, + _currentModifierKeys = 0, + + _isModifierKey = function (key) { + return key === "\uE008" || key === "\uE009" || key === "\uE00A" || key === "\uE03D"; + }, + + _isModifierKeyPressed = function (key) { + return _currentModifierKeys & _modifierKeyValues[_specialKeys[key].toUpperCase()]; + }, + + _sendKeys = function (session, keys) { + var keySequence = keys.split(''); + for (var i = 0; i < keySequence.length; i++) { + var key = keys[i]; + var actualKey = _translateKey(session, key); + + if (key === '\uE000') { + _clearModifierKeys(session); + } else { + if (_isModifierKey(key)) { + if (_isModifierKeyPressed(key)) { + _keyUp(session, actualKey); + } else { + _keyDown(session, actualKey); + } + } else { + if (_implicitShiftKeys.hasOwnProperty(actualKey)) { + session.getCurrentWindow().sendEvent("keydown", _translateKey(session, "\uE008")); + _pressKey(session, actualKey); + session.getCurrentWindow().sendEvent("keyup", _translateKey(session, "\uE008")); + } else { + if ((_currentModifierKeys & _modifierKeyValues.SHIFT) && _shiftKeys.hasOwnProperty(actualKey)) { + _pressKey(session, _shiftKeys[actualKey]); + } else { + _pressKey(session, actualKey); + } + } + } + } + } + }, + + _clearModifierKeys = function (session) { + if (_currentModifierKeys & _modifierKeyValues.SHIFT) { + _keyUp(session, _translateKey(session, "\uE008")); + } + if (_currentModifierKeys & _modifierKeyValues.CONTROL) { + _keyUp(session, _translateKey(session, "\uE009")); + } + if (_currentModifierKeys & _modifierKeyValues.ALT) { + _keyUp(session, _translateKey(session, "\uE00A")); + } + }, + + _updateModifierKeys = function (modifierKeyValue, on) { + if (on) { + _currentModifierKeys = _currentModifierKeys | modifierKeyValue; + } else { + _currentModifierKeys = _currentModifierKeys & ~modifierKeyValue; + } + }, + + _translateKey = function (session, key) { + var + actualKey = key, + phantomjskeys = session.getCurrentWindow().event.key; + if (_specialKeys.hasOwnProperty(key)) { + actualKey = _specialKeys[key]; + if (phantomjskeys.hasOwnProperty(actualKey)) { + actualKey = phantomjskeys[actualKey]; + } + } + return actualKey; + }, + + _pressKey = function (session, key) { + // translate WebDriver key value to key code. + _keyEvent(session, "keypress", key); + }, + + _keyDown = function (session, key) { + _keyEvent(session, "keydown", key); + if (key == _translateKey(session, "\uE008")) { + _updateModifierKeys(_modifierKeyValues.SHIFT, true); + } else if (key == _translateKey(session, "\uE009")) { + _updateModifierKeys(_modifierKeyValues.CONTROL, true); + } else if (key == _translateKey(session, "\uE00A")) { + _updateModifierKeys(_modifierKeyValues.ALT, true); + } + }, + + _keyUp = function (session, key) { + if (key == _translateKey(session, "\uE008")) { + _updateModifierKeys(_modifierKeyValues.SHIFT, false); + } else if (key == _translateKey(session, "\uE009")) { + _updateModifierKeys(_modifierKeyValues.CONTROL, false); + } else if (key == _translateKey(session, "\uE00A")) { + _updateModifierKeys(_modifierKeyValues.ALT, false); + } + _keyEvent(session, "keyup", key); + }, + + _mouseClick = function (session, coords) { + _mouseMove(session, coords); + _mouseButtonEvent(session, "click", "left"); + }, + + _mouseMove = function (session, coords) { + session.getCurrentWindow().sendEvent("mousemove", coords.x, coords.y); + _mousePos = { x: coords.x, y: coords.y }; + }, + + _mouseButtonDown = function (session, button) { + _mouseButtonEvent(session, "mousedown", button); + }, + + _mouseButtonUp = function (session, button) { + _mouseButtonEvent(session, "mouseUp", button); + }, + + _keyEvent = function (session, eventType, keyCode) { + eventType = eventType || "keypress"; + session.getCurrentWindow().sendEvent(eventType, keyCode, null, null, _currentModifierKeys); + }, + + _mouseButtonEvent = function (session, eventType, button) { + button = button || "left"; + eventType = eventType || "click"; + session.getCurrentWindow().sendEvent(eventType, + _mousePos.x, _mousePos.y, //< x, y + button, _currentModifierKeys); + }; + + return { + getCurrentCoordinates: function () { return _mousePos; }, + mouseClick: _mouseClick, + mouseMove: _mouseMove, + mouseButtonDown: _mouseButtonDown, + mouseButtonUp: _mouseButtonUp, + mouseButtonClick: _mouseButtonEvent, + sendKeys: _sendKeys, + clearModifierKeys: _clearModifierKeys + }; +}; diff --git a/src/ghostdriver/lastupdate b/src/ghostdriver/lastupdate new file mode 100644 index 0000000..5c6de61 --- /dev/null +++ b/src/ghostdriver/lastupdate @@ -0,0 +1,7 @@ +2014-02-12 23:42:59 + +commit 2af7099a9f5cf683ff565617be38b70318c9203f (HEAD, refs/remotes/origin/master, refs/remotes/origin/HEAD, refs/heads/master) +Author: Ivan De Marino +Date: Wed Feb 12 23:42:43 2014 +0000 + + Removing flaky (and pointless) test diff --git a/src/ghostdriver/logger.js b/src/ghostdriver/logger.js new file mode 100644 index 0000000..03369a8 --- /dev/null +++ b/src/ghostdriver/logger.js @@ -0,0 +1,109 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Init Console++ +require("./third_party/console++.js"); + +// Constants +const +separator = " - "; + +/** + * (Super-simple) Logger + * + * @param context {String} Logger context + */ +function Logger (context) { + var loggerObj, i; + + if (!context || context.length === 0) { + throw new Error("Invalid 'context' for Logger: " + context); + } + + loggerObj = { + debug : function(scope, message) { + console.debug(context + separator + + scope + + (message && message.length > 0 ? separator + message : "") + ); + }, + info : function(scope, message) { + console.info(context + separator + + scope + + (message && message.length > 0 ? separator + message : "") + ); + }, + warn : function(scope, message) { + console.warn(context + separator + + scope + + (message && message.length > 0 ? separator + message : "") + ); + }, + error : function(scope, message) { + console.error(context + separator + + scope + + (message && message.length > 0 ? separator + message : "") + ); + } + }; + + + return loggerObj; +} + +/** + * Export: Create Logger with Context + * + * @param context {String} Context of the new Logger + */ +exports.create = function (context) { + return new Logger(context); +}; + +/** + * Export: Add Log File. + * + * @param logFileName {String Name of the file were to output (append) the Logs. + */ +exports.addLogFile = function(logFileName) { + var fs = require("fs"), + f = fs.open(fs.absolute(logFileName), 'a'); + + // Append line to Log File + console.onOutput(function(msg, levelName) { + f.writeLine(msg); + f.flush(); + }); + + // Flush the Log File when process exits + phantom.aboutToExit.connect(f.flush); +}; + +/** + * Export: Console object + */ +exports.console = console; diff --git a/src/ghostdriver/main.js b/src/ghostdriver/main.js new file mode 100644 index 0000000..b9cfc4f --- /dev/null +++ b/src/ghostdriver/main.js @@ -0,0 +1,91 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var server = require("webserver").create(), //< webserver + router, //< router request handler + _log; //< logger for "main.js" + +// "ghostdriver" global namespace +ghostdriver = { + system : require("system"), + hub : require("./hub_register.js"), + logger : require("./logger.js"), + config : null, //< this will be set below + version : "1.2.0" +}; + +// create logger +_log = ghostdriver.logger.create("GhostDriver"); + +// Initialize the configuration +require("./config.js").init(ghostdriver.system.args); +ghostdriver.config = require("./config.js").get(); + +// Enable "strict mode" for the 'parseURI' library +require("./third_party/parseuri.js").options.strictMode = true; + +// Load all the core dependencies +// NOTE: We need to provide PhantomJS with the "require" module ASAP. This is a pretty s**t way to load dependencies +phantom.injectJs("session.js"); +phantom.injectJs("inputs.js"); +phantom.injectJs("request_handlers/request_handler.js"); +phantom.injectJs("request_handlers/status_request_handler.js"); +phantom.injectJs("request_handlers/shutdown_request_handler.js"); +phantom.injectJs("request_handlers/session_manager_request_handler.js"); +phantom.injectJs("request_handlers/session_request_handler.js"); +phantom.injectJs("request_handlers/webelement_request_handler.js"); +phantom.injectJs("request_handlers/router_request_handler.js"); +phantom.injectJs("webelementlocator.js"); + +try { + // HTTP Request Router + router = new ghostdriver.RouterReqHand(); + + // Start the server + if (server.listen(ghostdriver.config.port, { "keepAlive" : true }, router.handle)) { + _log.info("Main", "running on port " + server.port); + + // If a Selenium Grid HUB was provided, register to it! + if (ghostdriver.config.hub !== null) { + _log.info("Main", "registering to Selenium HUB"+ + " '" + ghostdriver.config.hub + "' version: " + ghostdriver.config.version + + " using '" + ghostdriver.config.ip + ":" + ghostdriver.config.port + "' with " + + ghostdriver.config.proxy + " as remote proxy."); + ghostdriver.hub.register(ghostdriver.config.ip, + ghostdriver.config.port, + ghostdriver.config.hub, + ghostdriver.config.proxy, + ghostdriver.config.version); + } + } else { + throw new Error("Could not start Ghost Driver"); + phantom.exit(1); + } +} catch (e) { + _log.error("main.fail", JSON.stringify(e)); + phantom.exit(1); +} diff --git a/src/ghostdriver/request_handlers/request_handler.js b/src/ghostdriver/request_handlers/request_handler.js new file mode 100644 index 0000000..0d55aca --- /dev/null +++ b/src/ghostdriver/request_handlers/request_handler.js @@ -0,0 +1,197 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.RequestHandler = function() { + // private: + var + _errors = require("./errors.js"), + _handle = function(request, response) { + // NOTE: Some language bindings result in a malformed "post" object. + // This might have to do with PhantomJS poor WebServer implementation. + // Here we override "request.post" with the "request.postRaw" that + // is usually left intact. + if (request.hasOwnProperty("postRaw")) { + request["post"] = request["postRaw"]; + } + + _decorateRequest(request); + _decorateResponse(response); + }, + + _reroute = function(request, response, prefixToRemove) { + // Store the original URL before re-routing in 'request.urlOriginal': + // This is done only for requests never re-routed. + // We don't want to override the original URL during a second re-routing. + if (typeof(request.urlOriginal) === "undefined") { + request.urlOriginal = request.url; + } + + // Rebase the "url" to start from AFTER the given prefix to remove + request.url = request.urlParsed.source.substr((prefixToRemove).length); + // Re-decorate the Request object + _decorateRequest(request); + + // Handle the re-routed request + this.handle(request, response); + }, + + _decorateRequest = function(request) { + // Normalize URL first + request.url = request.url.replace(/^\/wd\/hub/, ''); + // Then parse it + request.urlParsed = require("./third_party/parseuri.js").parse(request.url); + }, + + _writeJSONDecorator = function(obj) { + this.write(JSON.stringify(obj)); + }, + + _successDecorator = function(sessionId, value) { + this.statusCode = 200; + if (arguments.length > 0) { + // write something, only if there is something to write + this.writeJSONAndClose(_buildSuccessResponseBody(sessionId, value)); + } else { + this.closeGracefully(); + } + }, + + _writeAndCloseDecorator = function(body) { + this.setHeader("Content-Length", unescape(encodeURIComponent(body)).length); + this.write(body); + this.close(); + }, + + _writeJSONAndCloseDecorator = function(obj) { + var objStr = JSON.stringify(obj); + this.setHeader("Content-Length", unescape(encodeURIComponent(objStr)).length); + this.write(objStr); + this.close(); + }, + + _respondBasedOnResultDecorator = function(session, req, result) { + // Convert string to JSON + if (typeof(result) === "string") { + try { + result = JSON.parse(result); + } catch (e) { + // In case the conversion fails, report and "Invalid Command Method" error + _errors.handleInvalidReqInvalidCommandMethodEH(req, this); + } + } + + // In case the JSON doesn't contain the expected fields + if (result === null || + typeof(result) === "undefined" || + typeof(result) !== "object" || + typeof(result.status) === "undefined" || + typeof(result.value) === "undefined") { + _errors.handleFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.UnknownError, + "Command failed without producing the expected error report", + req, + this, + session); + return; + } + + // An error occurred but we got an error report to use + if (result.status !== 0) { + _errors.handleFailedCommandEH(result.status, + result.value.message, + req, + this, + session); + return; + } + + // If we arrive here, everything should be fine, birds are singing, the sky is blue + this.success(session.getId(), result.value); + }, + + _decorateResponse = function(response) { + response.setHeader("Cache", "no-cache"); + response.setHeader("Content-Type", "application/json;charset=UTF-8"); + response.writeAndClose = _writeAndCloseDecorator; + response.writeJSON = _writeJSONDecorator; + response.writeJSONAndClose = _writeJSONAndCloseDecorator; + response.success = _successDecorator; + response.respondBasedOnResult = _respondBasedOnResultDecorator; + }, + + _buildResponseBody = function(sessionId, value, statusCode) { + // Need to check for undefined to prevent errors when trying to return boolean false + if(typeof(value) === "undefined") value = {}; + return { + "sessionId" : sessionId || null, + "status" : statusCode || 0, //< '0' is Success + "value" : value + }; + }, + + _buildSuccessResponseBody = function(sessionId, value) { + return _buildResponseBody(sessionId, value, 0); //< '0' is Success + }, + + _getSessionCurrWindow = function(session, req) { + return _getSessionWindow(null, session, req); + }, + + _getSessionWindow = function(handleOrName, session, req) { + var win, + errorMsg; + + // Fetch the right window + win = handleOrName === null ? + session.getCurrentWindow() : //< current window + session.getWindow(handleOrName); //< window by handle + if (win !== null) { + return win; + } + + errorMsg = handleOrName === null ? + "Currently Window handle/name is invalid (closed?)" : + "Window handle/name '"+handleOrName+"' is invalid (closed?)"; + + // Report the error throwing the appropriate exception + throw _errors.createFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, errorMsg, req, session); + }; + + // public: + return { + handle : _handle, + reroute : _reroute, + buildResponseBody : _buildResponseBody, + buildSuccessResponseBody : _buildSuccessResponseBody, + decorateRequest : _decorateRequest, + decorateResponse : _decorateResponse, + errors : _errors, + getSessionWindow : _getSessionWindow, + getSessionCurrWindow : _getSessionCurrWindow + }; +}; diff --git a/src/ghostdriver/request_handlers/router_request_handler.js b/src/ghostdriver/request_handlers/router_request_handler.js new file mode 100644 index 0000000..05a3d5d --- /dev/null +++ b/src/ghostdriver/request_handlers/router_request_handler.js @@ -0,0 +1,105 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +/** + * This Class does first level routing: based on the REST Path, sends Request and Response to the right Request Handler. + */ +ghostdriver.RouterReqHand = function() { + // private: + const + _const = { + STATUS : "status", + SESSION : "session", + SESSIONS : "sessions", + SESSION_DIR : "/session/", + SHUTDOWN : "shutdown" + }; + + var + _protoParent = ghostdriver.RouterReqHand.prototype, + _statusRH = new ghostdriver.StatusReqHand(), + _shutdownRH = new ghostdriver.ShutdownReqHand(), + _sessionManRH = new ghostdriver.SessionManagerReqHand(), + _errors = _protoParent.errors, + _log = ghostdriver.logger.create("RouterReqHand"), + + _handle = function(req, res) { + var session, + sessionRH; + + // Invoke parent implementation + _protoParent.handle.call(this, req, res); + + _log.debug("_handle", JSON.stringify(req)); + + try { + if (req.urlParsed.chunks.length === 1 && req.urlParsed.file === _const.STATUS) { // GET '/status' + _statusRH.handle(req, res); + } else if (req.urlParsed.chunks.length === 1 && req.urlParsed.file === _const.SHUTDOWN) { // GET '/shutdown' + _shutdownRH.handle(req, res); + phantom.exit(); + } else if ((req.urlParsed.chunks.length === 1 && req.urlParsed.file === _const.SESSION) || // POST '/session' + (req.urlParsed.chunks.length === 1 && req.urlParsed.file === _const.SESSIONS) || // GET '/sessions' + req.urlParsed.directory === _const.SESSION_DIR) { // GET or DELETE '/session/:id' + _sessionManRH.handle(req, res); + } else if (req.urlParsed.chunks[0] === _const.SESSION) { // GET, POST or DELETE '/session/:id/...' + // Retrieve session + session = _sessionManRH.getSession(req.urlParsed.chunks[1]); + + if (session !== null) { + // Create a new Session Request Handler and re-route the request to it + sessionRH = _sessionManRH.getSessionReqHand(req.urlParsed.chunks[1]); + _protoParent.reroute.call(sessionRH, req, res, _const.SESSION_DIR + session.getId()); + } else { + throw _errors.createInvalidReqVariableResourceNotFoundEH(req); + } + } else { + throw _errors.createInvalidReqUnknownCommandEH(req); + } + } catch (e) { + _log.error("_handle.error", JSON.stringify(e)); + + if (typeof(e.handle) === "function") { + e.handle(res); + } else { + // This should never happen, if we handle all the possible error scenario + res.statusCode = 404; //< "404 Not Found" + res.setHeader("Content-Type", "text/plain"); + res.writeAndClose(e.name + " - " + e.message); + } + } + }; + + // public: + return { + handle : _handle + }; +}; +// prototype inheritance: +ghostdriver.RouterReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/request_handlers/session_manager_request_handler.js b/src/ghostdriver/request_handlers/session_manager_request_handler.js new file mode 100644 index 0000000..6d3bdde --- /dev/null +++ b/src/ghostdriver/request_handlers/session_manager_request_handler.js @@ -0,0 +1,186 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.SessionManagerReqHand = function() { + // private: + var + _protoParent = ghostdriver.SessionManagerReqHand.prototype, + _sessions = {}, //< will store key/value pairs like 'SESSION_ID : SESSION_OBJECT' + _sessionRHs = {}, + _errors = _protoParent.errors, + _CLEANUP_WINDOWLESS_SESSIONS_TIMEOUT = 300000, // 5 minutes + _log = ghostdriver.logger.create("SessionManagerReqHand"), + + _handle = function(req, res) { + _protoParent.handle.call(this, req, res); + + if (req.urlParsed.chunks.length === 1 && req.urlParsed.file === "session" && req.method === "POST") { + _postNewSessionCommand(req, res); + return; + } else if (req.urlParsed.chunks.length === 1 && req.urlParsed.file === "sessions" && req.method === "GET") { + _getActiveSessionsCommand(req, res); + return; + } else if (req.urlParsed.directory === "/session/") { + if (req.method === "GET") { + _getSessionCapabilitiesCommand(req, res); + } else if (req.method === "DELETE") { + _deleteSessionCommand(req, res); + } + return; + } + + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + }, + + _postNewSessionCommand = function(req, res) { + var newSession, + postObj, + redirectToHost; + + try { + postObj = JSON.parse(req.post); + } catch (e) { + // If the parsing has failed, the error is reported at the end + } + + if (typeof(postObj) === "object" && + typeof(postObj.desiredCapabilities) === "object") { + // Create and store a new Session + newSession = new ghostdriver.Session(postObj.desiredCapabilities); + _sessions[newSession.getId()] = newSession; + + _log.info("_postNewSessionCommand", "New Session Created: " + newSession.getId()); + + // Return newly created Session Capabilities + res.success(newSession.getId(), newSession.getCapabilities()); + return; + } + + throw _errors.createInvalidReqMissingCommandParameterEH(req); + }, + + _getActiveSessionsCommand = function(req, res) { + var activeSessions = [], + sessionId; + + // Create array of format '[{ "id" : SESSION_ID, "capabilities" : SESSION_CAPABILITIES_OBJECT }]' + for (sessionId in _sessions) { + activeSessions.push({ + "id" : sessionId, + "capabilities" : _sessions[sessionId].getCapabilities() + }); + } + + res.success(null, activeSessions); + }, + + _deleteSession = function(sessionId) { + if (typeof(_sessions[sessionId]) !== "undefined") { + // Prepare the session to be deleted + _sessions[sessionId].aboutToDelete(); + // Delete the session and the handler + delete _sessions[sessionId]; + delete _sessionRHs[sessionId]; + } + }, + + _deleteSessionCommand = function(req, res) { + var sId = req.urlParsed.file; + + if (sId === "") + throw _errors.createInvalidReqMissingCommandParameterEH(req); + + if (typeof(_sessions[sId]) !== "undefined") { + _deleteSession(sId); + res.success(sId); + } else { + throw _errors.createInvalidReqVariableResourceNotFoundEH(req); + } + }, + + _getSessionCapabilitiesCommand = function(req, res) { + var sId = req.urlParsed.file, + session; + + if (sId === "") + throw _errors.createInvalidReqMissingCommandParameterEH(req); + + session = _getSession(sId); + if (session !== null) { + res.success(sId, _sessions[sId].getCapabilities()); + } else { + throw _errors.createInvalidReqVariableResourceNotFoundEH(req); + } + }, + + _getSession = function(sessionId) { + if (typeof(_sessions[sessionId]) !== "undefined") { + return _sessions[sessionId]; + } + return null; + }, + + _getSessionReqHand = function(sessionId) { + if (_getSession(sessionId) !== null) { + // The session exists: what about the relative Session Request Handler? + if (typeof(_sessionRHs[sessionId]) === "undefined") { + _sessionRHs[sessionId] = new ghostdriver.SessionReqHand(_getSession(sessionId)); + } + return _sessionRHs[sessionId]; + } + return null; + }, + + _cleanupWindowlessSessions = function() { + var sId; + + // Do this cleanup only if there are sessions + if (Object.keys(_sessions).length > 0) { + _log.info("_cleanupWindowlessSessions", "Asynchronous Sessions clean-up phase starting NOW"); + for (sId in _sessions) { + if (_sessions[sId].getWindowsCount() === 0) { + _deleteSession(sId); + _log.info("_cleanupWindowlessSessions", "Deleted Session '"+sId+"', because windowless"); + } + } + } + }; + + // Regularly cleanup un-used sessions + setInterval(_cleanupWindowlessSessions, _CLEANUP_WINDOWLESS_SESSIONS_TIMEOUT); + + // public: + return { + handle : _handle, + getSession : _getSession, + getSessionReqHand : _getSessionReqHand + }; +}; +// prototype inheritance: +ghostdriver.SessionManagerReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/request_handlers/session_request_handler.js b/src/ghostdriver/request_handlers/session_request_handler.js new file mode 100644 index 0000000..8ca3ccd --- /dev/null +++ b/src/ghostdriver/request_handlers/session_request_handler.js @@ -0,0 +1,892 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +Copyright (c) 2014, Alex Anderson <@alxndrsn> +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.SessionReqHand = function(session) { + // private: + const + _const = { + URL : "url", + ELEMENT : "element", + ELEMENTS : "elements", + ELEMENT_DIR : "/element/", + ACTIVE : "active", + TITLE : "title", + WINDOW : "window", + CURRENT : "current", + SIZE : "size", + POSITION : "position", + MAXIMIZE : "maximize", + FORWARD : "forward", + BACK : "back", + REFRESH : "refresh", + EXECUTE : "execute", + EXECUTE_ASYNC : "execute_async", + SCREENSHOT : "screenshot", + TIMEOUTS : "timeouts", + TIMEOUTS_DIR : "/timeouts/", + ASYNC_SCRIPT : "async_script", + IMPLICIT_WAIT : "implicit_wait", + WINDOW_HANDLE : "window_handle", + WINDOW_HANDLES : "window_handles", + FRAME : "frame", + SOURCE : "source", + COOKIE : "cookie", + KEYS : "keys", + FILE : "file", + MOVE_TO : "moveto", + CLICK : "click", + BUTTON_DOWN : "buttondown", + BUTTON_UP : "buttonup", + DOUBLE_CLICK : "doubleclick", + PHANTOM_DIR : "/phantom/", + PHANTOM_EXEC : "execute", + LOG : "log", + TYPES : "types" + }; + + var + _session = session, + _protoParent = ghostdriver.SessionReqHand.prototype, + _locator = new ghostdriver.WebElementLocator(session), + _errors = _protoParent.errors, + _log = ghostdriver.logger.create("SessionReqHand"), + + _handle = function(req, res) { + var element; + + _protoParent.handle.call(this, req, res); + + // Handle "/url" GET and POST + if (req.urlParsed.file === _const.URL) { //< ".../url" + if (req.method === "GET") { + _getUrlCommand(req, res); + } else if (req.method === "POST") { + _postUrlCommand(req, res); + } + return; + } else if (req.urlParsed.file === _const.SCREENSHOT && req.method === "GET") { + _getScreenshotCommand(req, res); + return; + } else if (req.urlParsed.file === _const.WINDOW) { //< ".../window" + if (req.method === "DELETE") { + _deleteWindowCommand(req, res); //< close window + } else if (req.method === "POST") { + _postWindowCommand(req, res); //< change focus to the given window + } + return; + } else if (req.urlParsed.chunks[0] === _const.WINDOW) { + _doWindowHandleCommands(req, res); + return; + } else if (req.urlParsed.file === _const.ELEMENT && req.method === "POST" && req.urlParsed.chunks.length === 1) { //< ".../element" + _locator.handleLocateCommand(req, res, _locator.locateElement); + return; + } else if (req.urlParsed.file === _const.ELEMENTS && req.method === "POST" && req.urlParsed.chunks.length === 1) { //< ".../elements" + _locator.handleLocateCommand(req, res, _locator.locateElements); + return; + } else if (req.urlParsed.chunks[0] === _const.ELEMENT && req.urlParsed.chunks[1] === _const.ACTIVE && req.method === "POST") { //< ".../element/active" + _locator.handleLocateCommand(req, res, _locator.locateActiveElement); + return; + } else if (req.urlParsed.chunks[0] === _const.ELEMENT) { //< ".../element/:elementId/COMMAND" + // Get the WebElementRH and, if found, re-route request to it + element = new ghostdriver.WebElementReqHand(req.urlParsed.chunks[1], _session); + if (element !== null) { + _protoParent.reroute.call(element, req, res, _const.ELEMENT_DIR + req.urlParsed.chunks[1]); + } else { + throw _errors.createInvalidReqVariableResourceNotFoundEH(req); + } + return; + } else if (req.urlParsed.file === _const.TITLE && req.method === "GET") { //< ".../title" + // Get the current Page title + _getTitleCommand(req, res); + return; + } else if (req.urlParsed.file === _const.KEYS && req.method === "POST") { + _postKeysCommand(req, res); + return; + } else if (req.urlParsed.file === _const.FORWARD && req.method === "POST") { + _forwardCommand(req, res); + return; + } else if (req.urlParsed.file === _const.BACK && req.method === "POST") { + _backCommand(req, res); + return; + } else if (req.urlParsed.file === _const.REFRESH && req.method === "POST") { + _refreshCommand(req, res); + return; + } else if (req.urlParsed.file === _const.EXECUTE && req.urlParsed.directory === "/" && req.method == "POST") { + _executeCommand(req, res); + return; + } else if (req.urlParsed.file === _const.EXECUTE_ASYNC && req.method === "POST") { + _executeAsyncCommand(req, res); + return; + } else if ((req.urlParsed.file === _const.TIMEOUTS || req.urlParsed.directory === _const.TIMEOUTS_DIR) && req.method === "POST") { + _postTimeout(req, res); + return; + } else if (req.urlParsed.file === _const.WINDOW_HANDLE && req.method === "GET") { + _getWindowHandle(req, res); + return; + } else if (req.urlParsed.file === _const.WINDOW_HANDLES && req.method === "GET") { + _getWindowHandles(req, res); + return; + } else if (req.urlParsed.file === _const.FRAME && req.method === "POST") { + _postFrameCommand(req, res); + return; + } else if (req.urlParsed.file === _const.SOURCE && req.method === "GET") { + _getSourceCommand(req, res); + return; + } else if (req.urlParsed.file === _const.MOVE_TO && req.method === "POST") { + _postMouseMoveToCommand(req, res); + return; + } else if (req.urlParsed.file === _const.PHANTOM_EXEC && req.urlParsed.directory === _const.PHANTOM_DIR && req.method === "POST") { + _executePhantomJS(req, res); + return; + } else if (req.urlParsed.file === _const.CLICK && req.method === "POST") { + _postMouseClickCommand(req, res, "click"); + return; + } else if (req.urlParsed.file === _const.BUTTON_DOWN && req.method === "POST") { + _postMouseClickCommand(req, res, "mousedown"); + return; + } else if (req.urlParsed.file === _const.BUTTON_UP && req.method === "POST") { + _postMouseClickCommand(req, res, "mouseup"); + return; + } else if (req.urlParsed.file === _const.DOUBLE_CLICK && req.method === "POST") { + _postMouseClickCommand(req, res, "doubleclick"); + return; + } else if (req.urlParsed.chunks[0] === _const.COOKIE) { + if (req.method === "POST") { + _postCookieCommand(req, res); + } else if (req.method === "GET") { + _getCookieCommand(req, res); + } else if(req.method === "DELETE") { + _deleteCookieCommand(req, res); + } + return; + } else if (req.urlParsed.chunks[0] === _const.LOG && req.method === "POST") { //< ".../log" + _postLog(req, res); + return; + } else if (req.urlParsed.chunks[0] === _const.LOG && req.urlParsed.chunks[1] === _const.TYPES && req.method === "GET") { //< ".../log/types" + _getLogTypes(req, res); + return; + } else if (req.urlParsed.chunks[0] === _const.LOG && _session.getLogTypes().indexOf(req.urlParsed.chunks[1]) >= 0 && req.method === "GET") { //< ".../log/LOG_TYPE" + _getLog(req, res, req.urlParsed.chunks[1]); + } else if (req.urlParsed.file == _const.FILE && req.method === "POST") { + _postUploadFileCommand(req, res); + return; + } + + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + }, + + _postUploadFileCommand = function(req, res) { + var postObj = JSON.parse(req.post), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req), + inputFileSelector = postObj.selector, + filePath = postObj.filepath; + + _log.debug("_postUploadFileCommand about to upload file", inputFileSelector, filePath) + currWindow.uploadFile(inputFileSelector, filePath); + res.success(_session.getId()) + }, + + _createOnSuccessHandler = function(res) { + return function (status) { + _log.debug("_SuccessHandler", "status: " + status); + res.success(_session.getId()); + }; + }, + + _doWindowHandleCommands = function(req, res) { + var windowHandle, + command, + targetWindow; + + _log.debug("_doWindowHandleCommands", JSON.stringify(req)); + + // Ensure all the parameters are provided + if (req.urlParsed.chunks.length === 3) { + windowHandle = req.urlParsed.chunks[1]; + command = req.urlParsed.chunks[2]; + + // Fetch the right window + if (windowHandle === _const.CURRENT) { + targetWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + } else { + targetWindow = _protoParent.getSessionWindow.call(this, windowHandle, _session, req); + } + + // Act on the window (page) + if(command === _const.SIZE && req.method === "POST") { + _postWindowSizeCommand(req, res, targetWindow); + return; + } else if(command === _const.SIZE && req.method === "GET") { + _getWindowSizeCommand(req, res, targetWindow); + return; + } else if(command === _const.POSITION && req.method === "POST") { + _postWindowPositionCommand(req, res, targetWindow); + return; + } else if(command === _const.POSITION && req.method === "GET") { + _getWindowPositionCommand(req, res, targetWindow); + return; + } else if(command === _const.MAXIMIZE && req.method === "POST") { + _postWindowMaximizeCommand(req, res, targetWindow); + return; + } + + // No command matched: error + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postWindowSizeCommand = function(req, res, targetWindow) { + var params = JSON.parse(req.post), + newWidth = params.width, + newHeight = params.height; + + // If width/height are passed in string, force them to numbers + if (typeof(params.width) === "string") { + newWidth = parseInt(params.width, 10); + } + if (typeof(params.height) === "string") { + newHeight = parseInt(params.height, 10); + } + + // If a number was not found, the command is + if (isNaN(newWidth) || isNaN(newHeight)) { + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + } + + targetWindow.viewportSize = { + width : newWidth, + height : newHeight + }; + res.success(_session.getId()); + }, + + _getWindowSizeCommand = function(req, res, targetWindow) { + // Returns response in the format "{width: number, height: number}" + res.success(_session.getId(), targetWindow.viewportSize); + }, + + _postWindowPositionCommand = function(req, res, targetWindow) { + var params = JSON.parse(req.post), + newX = params.x, + newY = params.y; + + // If width/height are passed in string, force them to numbers + if (typeof(params.x) === "string") { + newX = parseInt(params.x, 10); + } + if (typeof(params.y) === "string") { + newY = parseInt(params.y, 10); + } + + // If a number was not found, the command is + if (isNaN(newX) || isNaN(newY)) { + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + } + + // NOTE: Nothing to do! PhantomJS is headless. :) + res.success(_session.getId()); + }, + + _getWindowPositionCommand = function(req, res, targetWindow) { + // Returns response in the format "{width: number, height: number}" + res.success(_session.getId(), { x : 0, y : 0 }); + }, + + _postWindowMaximizeCommand = function(req, res, targetWindow) { + // NOTE: PhantomJS is headless, so there is no "screen" to maximize to + // or "window" resize to that. + // + // NOTE: The most common screen resolution used online is currently: 1366x768 + // See http://gs.statcounter.com/#resolution-ww-monthly-201307-201312. + targetWindow.viewportSize = { + width : 1366, + height : 768 + }; + + res.success(_session.getId()); + }, + + _postKeysCommand = function(req, res) { + var activeEl = _locator.locateActiveElement(); + var elReqHand = new ghostdriver.WebElementReqHand(activeEl.value, _session); + elReqHand.postValueCommand(req, res); + }, + + _refreshCommand = function(req, res) { + var successHand = _createOnSuccessHandler(res), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + currWindow.execFuncAndWaitForLoad( + function() { currWindow.reload(); }, + successHand, + successHand); //< We don't care if 'refresh' fails + }, + + _backCommand = function(req, res) { + var successHand = _createOnSuccessHandler(res), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + if (currWindow.canGoBack) { + currWindow.execFuncAndWaitForLoad( + function() { currWindow.goBack(); }, + successHand, + successHand); //< We don't care if 'back' fails + } else { + // We can't go back, and that's ok + successHand(); + } + }, + + _forwardCommand = function(req, res) { + var successHand = _createOnSuccessHandler(res), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + if (currWindow.canGoForward) { + currWindow.execFuncAndWaitForLoad( + function() { currWindow.goForward(); }, + successHand, + successHand); //< We don't care if 'forward' fails + } else { + // We can't go forward, and that's ok + successHand(); + } + }, + + _executeCommand = function(req, res) { + var postObj = JSON.parse(req.post), + result, + timer, + scriptTimeout = _session.getScriptTimeout(), + timedOut = false; + + if (typeof(postObj) === "object" && postObj.script && postObj.args) { + // Execute script, but within a limited timeframe + timer = setTimeout(function() { + // The script didn't return within the expected timeframe + timedOut = true; + _errors.handleFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.Timeout, + "Script didn't return within " + scriptTimeout + "ms", + req, + res, + _session); + }, scriptTimeout); + + // Launch the actual script + result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + postObj.script, + postObj.args, + true); + + // If we are here, we don't need the timer anymore + clearTimeout(timer); + + // Respond with result ONLY if this hasn't ALREADY timed-out + if (!timedOut) { + res.respondBasedOnResult(_session, req, result); + } + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _executeAsyncCommand = function(req, res) { + var postObj = JSON.parse(req.post); + + _log.debug("_executeAsyncCommand", JSON.stringify(postObj)); + + if (typeof(postObj) === "object" && postObj.script && postObj.args) { + _protoParent.getSessionCurrWindow.call(this, _session, req).setOneShotCallback("onCallback", function() { + _log.debug("_executeAsyncCommand.callbackArguments", JSON.stringify(arguments)); + + res.respondBasedOnResult(_session, req, arguments[0]); + }); + + _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + "function(script, args, timeout) { " + + "return (" + require("./webdriver_atoms.js").get("execute_async_script") + ")" + + "(script, args, timeout, callPhantom, true); " + + "}", + postObj.script, + postObj.args, + _session.getScriptTimeout()); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _getWindowHandle = function (req, res) { + var handle; + + // Get current window handle + handle = _session.getCurrentWindowHandle(); + + if (handle !== null) { + res.success(_session.getId(), handle); + } else { + throw _errors.createFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, + "Current window handle invalid (closed?)", + req, + _session); + } + }, + + _getWindowHandles = function(req, res) { + res.success(_session.getId(), _session.getWindowHandles()); + }, + + _getScreenshotCommand = function(req, res) { + var rendering = _protoParent.getSessionCurrWindow.call(this, _session, req).renderBase64("png"); + res.success(_session.getId(), rendering); + }, + + _getUrlCommand = function(req, res) { + // Get the URL at which the Page currently is + var result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return location.toString()", + []); + + res.respondBasedOnResult(_session, res, result); + }, + + _postUrlCommand = function(req, res) { + // Load the given URL in the Page + var postObj = JSON.parse(req.post), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + _log.debug("_postUrlCommand", "Session '"+ _session.getId() +"' is about to load URL: " + postObj.url); + + if (typeof(postObj) === "object" && postObj.url) { + // Switch to the main frame first + currWindow.switchToMainFrame(); + + // Load URL and wait for load to finish (or timeout) + currWindow.execFuncAndWaitForLoad(function() { + currWindow.open(postObj.url.trim()); + }, + _createOnSuccessHandler(res), //< success + function(errMsg) { //< failure/timeout + var errCode = errMsg === "timeout" + ? _errors.FAILED_CMD_STATUS_CODES.Timeout + : _errors.FAILED_CMD_STATUS_CODES.UnknownError; + + // Report error + _errors.handleFailedCommandEH(errCode, + "URL '" + postObj.url + "' didn't load. Error: '" + errMsg + "'", + req, + res, + _session); + }); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postTimeout = function(req, res) { + var postObj = JSON.parse(req.post); + + // Normalize the call: the "type" is read from the URL, not a POST parameter + if (req.urlParsed.file === _const.IMPLICIT_WAIT) { + postObj["type"] = _session.timeoutNames.IMPLICIT; + } else if (req.urlParsed.file === _const.ASYNC_SCRIPT) { + postObj["type"] = _session.timeoutNames.SCRIPT; + } + + if (typeof(postObj["type"]) === "string" && typeof(postObj["ms"]) === "number") { + + _log.debug("_postTimeout", JSON.stringify(postObj)); + + // Set the right timeout on the Session + switch(postObj["type"]) { + case _session.timeoutNames.SCRIPT: + _session.setScriptTimeout(postObj["ms"]); + break; + case _session.timeoutNames.IMPLICIT: + _session.setImplicitTimeout(postObj["ms"]); + break; + case _session.timeoutNames.PAGE_LOAD: + _session.setPageLoadTimeout(postObj["ms"]); + break; + default: + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + + res.success(_session.getId()); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postFrameCommand = function(req, res) { + var postObj = JSON.parse(req.post), + frameName, + framePos, + switched = false, + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + _log.debug("_postFrameCommand", "Current frames count: " + currWindow.framesCount); + + if (typeof(postObj) === "object" && typeof(postObj.id) !== "undefined") { + if(postObj.id === null) { + _log.debug("_postFrameCommand", "Switching to 'null' (main frame)"); + + // Reset focus on the topmost (main) Frame + currWindow.switchToMainFrame(); + switched = true; + } else if (typeof(postObj.id) === "number") { + _log.debug("_postFrameCommand", "Switching to frame number: " + postObj.id); + + // Switch frame by "index" + switched = currWindow.switchToFrame(postObj.id); + } else if (typeof(postObj.id) === "string") { + // Switch frame by "name" or by "id" + _log.debug("_postFrameCommand", "Switching to frame #id: " + postObj.id); + + switched = currWindow.switchToFrame(postObj.id); + + // If we haven't switched, let's try to find the frame "name" using it's "id" + if (!switched) { + // fetch the frame "name" via "id" + frameName = currWindow.evaluate(function(frameId) { + var el = null; + el = document.querySelector('#'+frameId); + if (el !== null) { + return el.name; + } + + return null; + }, postObj.id); + + _log.debug("_postFrameCommand", "Failed to switch by #id, trying by name: " + frameName); + + // Switch frame by "name" + if (frameName !== null) { + switched = currWindow.switchToFrame(frameName); + } + + if (!switched) { + // fetch the frame "position" via "id" + framePos = currWindow.evaluate(function(frameIdOrName) { + var allFrames = document.querySelectorAll("frame,iframe"), + theFrame = document.querySelector('#'+frameIdOrName) || document.querySelector('[name='+frameIdOrName+']'), + i; + + for (i = allFrames.length -1; i >= 0; --i) { + if (allFrames[i].contentWindow === theFrame.contentWindow) { + return i; + } + } + }, postObj.id); + + if (framePos >= 0) { + _log.debug("_postFrameCommand", "Failed to switch by #id or name, trying by position: "+framePos); + switched = currWindow.switchToFrame(framePos); + } else { + _log.warn("_postFrameCommand", "Unable to locate the Frame!"); + } + } + } + } else if (typeof(postObj.id) === "object" && typeof(postObj.id["ELEMENT"]) === "string") { + _log.debug("_postFrameCommand.element", JSON.stringify(postObj.id)); + + // Will use the Element JSON to find the frame name + frameName = currWindow.evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "if (!arguments[0].name && !arguments[0].id) { " + + " arguments[0].name = '_random_name_id_' + new Date().getTime(); " + + " arguments[0].id = arguments[0].name; " + + "} " + + "return arguments[0].name || arguments[0].id;", + [postObj.id]); + + _log.debug("_postFrameCommand.frameName", frameName.value); + + // If a frame name (or id) is found for the given ELEMENT, we + // "re-call" this very function, changing the `post` property + // on the `req` object. The `post` will contain this time + // the frame name (or id) that was found. + if (frameName && frameName.value) { + req.post = "{\"id\" : \"" + frameName.value + "\"}"; + _postFrameCommand.call(this, req, res); + return; + } + } else { + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + } + + // Send a positive response if the switch was successful + if (switched) { + res.success(_session.getId()); + } else { + // ... otherwise, throw the appropriate exception + throw _errors.createFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.NoSuchFrame, + "Unable to switch to frame", + req, + _session); + } + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _getSourceCommand = function(req, res) { + var source = _protoParent.getSessionCurrWindow.call(this, _session, req).frameContent; + res.success(_session.getId(), source); + }, + + _postMouseMoveToCommand = function(req, res) { + var postObj = JSON.parse(req.post), + coords = { x: 0, y: 0 }, + elementLocation, + elementSize, + elementSpecified = false, + offsetSpecified = false; + + if (typeof postObj === "object") { + elementSpecified = postObj.element && postObj.element != null; + offsetSpecified = typeof postObj.xoffset !== "undefined" && typeof postObj.yoffset !== "undefined"; + } + // Check that either an Element ID or an X-Y Offset was provided + if (elementSpecified || offsetSpecified) { + _log.debug("_postMouseMoveToCommand", "element: " + elementSpecified + ", offset: " + offsetSpecified); + + // If an Element was provided... + if (elementSpecified) { + // Get Element's Location and add it to the coordinates + var requestHandler = new ghostdriver.WebElementReqHand(postObj.element, _session); + elementLocation = requestHandler.getLocationInView(); + elementSize = requestHandler.getSize(); + // If the Element has a valid location + if (elementLocation !== null) { + coords.x = elementLocation.x; + coords.y = elementLocation.y; + } + } else { + coords = _session.inputs.getCurrentCoordinates(); + } + + _log.debug("_postMouseMoveToCommand", "initial coordinates: (" + coords.x + "," + coords.y + ")"); + + if (elementSpecified && !offsetSpecified && elementSize !== null) { + coords.x += Math.floor(elementSize.width / 2); + coords.y += Math.floor(elementSize.height / 2); + } else { + // Add up the offset (if any) + coords.x += postObj.xoffset || 0; + coords.y += postObj.yoffset || 0; + } + + _log.debug("_postMouseMoveToCommand", "coordinates adjusted to: (" + coords.x + "," + coords.y + ")"); + + // Send the Mouse Move as native event + _session.inputs.mouseMove(_session, coords); + res.success(_session.getId()); + } else { + // Neither "element" nor "xoffset/yoffset" were provided + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postMouseClickCommand = function(req, res, clickType) { + var postObj = {}, + mouseButton = "left"; + // normalize click + clickType = clickType || "click"; + + // The protocol allows language bindings to send an empty string (or no data at all) + if (req.post && req.post.length > 0) { + postObj = JSON.parse(req.post); + } + + // Check that either an Element ID or an X-Y Offset was provided + if (typeof(postObj) === "object") { + // Determine which button to click + if (typeof(postObj.button) === "number") { + // 0 is left, 1 is middle, 2 is right + mouseButton = (postObj.button === 2) ? "right" : (postObj.button === 1) ? "middle" : "left"; + } + // Send the Mouse Click as native event + _session.inputs.mouseButtonClick(_session, clickType, mouseButton); + res.success(_session.getId()); + } else { + // Neither "element" nor "xoffset/yoffset" were provided + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postCookieCommand = function(req, res) { + var postObj = JSON.parse(req.post || "{}"), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req); + + // If the page has not loaded anything yet, setting cookies is forbidden + if (currWindow.url.indexOf("about:blank") === 0) { + // Something else went wrong + _errors.handleFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.UnableToSetCookie, + "Unable to set Cookie: no URL has been loaded yet", + req, + res, + _session); + return; + } + + if (postObj.cookie) { + // JavaScript deals with Timestamps in "milliseconds since epoch": normalize! + if (postObj.cookie.expiry) { + postObj.cookie.expiry *= 1000; + } + + // If the cookie is expired OR if it was successfully added + if ((postObj.cookie.expiry && postObj.cookie.expiry <= new Date().getTime()) || + currWindow.addCookie(postObj.cookie)) { + // Notify success + res.success(_session.getId()); + } else { + // Something went wrong while trying to set the cookie + if (currWindow.url.indexOf(postObj.cookie.domain) < 0) { + // Domain mismatch + _errors.handleFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.InvalidCookieDomain, + "Can only set Cookies for the current domain", + req, + res, + _session); + } else { + // Something else went wrong + _errors.handleFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.UnableToSetCookie, + "Unable to set Cookie", + req, + res, + _session); + } + } + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _getCookieCommand = function(req, res) { + // Get all the cookies the session at current URL can see/access + res.success( + _session.getId(), + _protoParent.getSessionCurrWindow.call(this, _session, req).cookies); + }, + + _deleteCookieCommand = function(req, res) { + if (req.urlParsed.chunks.length === 2) { + // delete only 1 cookie among the one visible to this page + _protoParent.getSessionCurrWindow.call(this, _session, req).deleteCookie(req.urlParsed.chunks[1]); + } else { + // delete all the cookies visible to this page + _protoParent.getSessionCurrWindow.call(this, _session, req).clearCookies(); + } + res.success(_session.getId()); + }, + + _deleteWindowCommand = function(req, res) { + var params = JSON.parse(req.post || "{}"), //< in case nothing is posted at all + closed = false; + + // Use the "name" parameter if it was provided + if (typeof(params) === "object" && params.name) { + closed = _session.closeWindow(params.name); + } else { + closed = _session.closeCurrentWindow(); + } + + // Report a success if we manage to close the window, + // otherwise throw a Failed Command Error + if (closed) { + res.success(_session.getId()); + } else { + throw _errors.createFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, + "Unable to close window (closed already?)", + req, + _session); + } + }, + + _postWindowCommand = function(req, res) { + var params = JSON.parse(req.post); + + if (typeof(params) === "object" && typeof(params.name) === "string") { + // Report a success if we manage to switch the current window, + // otherwise throw a Failed Command Error + if (_session.switchToWindow(params.name)) { + res.success(_session.getId()); + } else { + throw _errors.createFailedCommandEH(_errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, + "Unable to switch to window (closed?)", + req, + _session); + } + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _getTitleCommand = function(req, res) { + res.success(_session.getId(), _protoParent.getSessionCurrWindow.call(this, _session, req).title); + }, + + _executePhantomJS = function(req, res) { + var params = JSON.parse(req.post); + if (typeof(params) === "object" && params.script && params.args) { + res.success(_session.getId(), _session.executePhantomJS(_protoParent.getSessionCurrWindow.call(this, _session, req), params.script, params.args)); + } else { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + }, + + _postLog = function (req, res) { + var params = JSON.parse(req.post); + if (!params.type || _session.getLogTypes().indexOf(params.type) < 0) { + throw _errors.createInvalidReqMissingCommandParameterEH(req); + } + _getLog(req, res, params.type); + }, + + _getLogTypes = function (req, res) { + res.success(_session.getId(), _session.getLogTypes()); + }, + + _getLog = function (req, res, logType) { + res.success(_session.getId(), _session.getLog(logType)); + }; + + // public: + return { + handle : _handle, + getSessionId : function() { return _session.getId(); } + }; +}; +// prototype inheritance: +ghostdriver.SessionReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/request_handlers/shutdown_request_handler.js b/src/ghostdriver/request_handlers/shutdown_request_handler.js new file mode 100644 index 0000000..865c232 --- /dev/null +++ b/src/ghostdriver/request_handlers/shutdown_request_handler.js @@ -0,0 +1,61 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +Copyright (c) 2014, Jim Evans - Salesforce.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.ShutdownReqHand = function() { + // private: + var + _protoParent = ghostdriver.ShutdownReqHand.prototype, + _log = ghostdriver.logger.create("ShutdownReqHand"), + + _handle = function(req, res) { + _log.info("_handle", "About to shutdown"); + + _protoParent.handle.call(this, req, res); + + // Any HTTP Request Method will be accepted for this command. Some drivers like HEAD for example... + if (req.urlParsed.file === "shutdown") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html;charset=UTF-8"); + res.setHeader("Content-Length", 36); + res.write("Closing..."); + res.close(); + return; + } + + throw _protoParent.errors.createInvalidReqInvalidCommandMethodEH(req); + }; + + // public: + return { + handle : _handle + }; +}; +// prototype inheritance: +ghostdriver.ShutdownReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/request_handlers/status_request_handler.js b/src/ghostdriver/request_handlers/status_request_handler.js new file mode 100644 index 0000000..3868102 --- /dev/null +++ b/src/ghostdriver/request_handlers/status_request_handler.js @@ -0,0 +1,64 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.StatusReqHand = function() { + // private: + const + _statusObj = { + "build" : { + "version" : ghostdriver.version + }, + "os" : { + "name" : ghostdriver.system.os.name, + "version" : ghostdriver.system.os.version, + "arch" : ghostdriver.system.os.architecture + } + }; + + var + _protoParent = ghostdriver.StatusReqHand.prototype, + + _handle = function(req, res) { + _protoParent.handle.call(this, req, res); + + if (req.method === "GET" && req.urlParsed.file === "status") { + res.success(null, _statusObj); + return; + } + + throw _protoParent.errors.createInvalidReqInvalidCommandMethodEH(req); + }; + + // public: + return { + handle : _handle + }; +}; +// prototype inheritance: +ghostdriver.StatusReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/request_handlers/webelement_request_handler.js b/src/ghostdriver/request_handlers/webelement_request_handler.js new file mode 100644 index 0000000..0d3fdff --- /dev/null +++ b/src/ghostdriver/request_handlers/webelement_request_handler.js @@ -0,0 +1,516 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +Copyright (c) 2014, Alex Anderson <@alxndrsn> +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.WebElementReqHand = function(idOrElement, session) { + // private: + const + _const = { + ELEMENT : "element", + ELEMENTS : "elements", + VALUE : "value", + SUBMIT : "submit", + DISPLAYED : "displayed", + ENABLED : "enabled", + ATTRIBUTE : "attribute", + NAME : "name", + CLICK : "click", + SELECTED : "selected", + CLEAR : "clear", + CSS : "css", + TEXT : "text", + EQUALS : "equals", + LOCATION : "location", + LOCATION_IN_VIEW : "location_in_view", + SIZE : "size" + }; + + var + _id = ((typeof(idOrElement) === "object") ? idOrElement["ELEMENT"] : idOrElement), + _session = session, + _locator = new ghostdriver.WebElementLocator(_session), + _protoParent = ghostdriver.WebElementReqHand.prototype, + _errors = _protoParent.errors, + _log = ghostdriver.logger.create("WebElementReqHand"), + + _handle = function(req, res) { + _protoParent.handle.call(this, req, res); + + if (req.urlParsed.file === _const.ELEMENT && req.method === "POST") { + _locator.handleLocateCommand(req, res, _locator.locateElement, _getJSON()); + return; + } else if (req.urlParsed.file === _const.ELEMENTS && req.method === "POST") { + _locator.handleLocateCommand(req, res, _locator.locateElements, _getJSON()); + return; + } else if (req.urlParsed.file === _const.VALUE && req.method === "POST") { + _postValueCommand(req, res); + return; + } else if (req.urlParsed.file === _const.SUBMIT && req.method === "POST") { + _postSubmitCommand(req, res); + return; + } else if (req.urlParsed.file === _const.DISPLAYED && req.method === "GET") { + _getDisplayedCommand(req, res); + return; + } else if (req.urlParsed.file === _const.ENABLED && req.method === "GET") { + _getEnabledCommand(req, res); + return; + } else if (req.urlParsed.chunks[0] === _const.ATTRIBUTE && req.method === "GET") { + _getAttributeCommand(req, res); + return; + } else if (req.urlParsed.file === _const.NAME && req.method === "GET") { + _getNameCommand(req, res); + return; + } else if (req.urlParsed.file === _const.CLICK && req.method === "POST") { + _postClickCommand(req, res); + return; + } else if (req.urlParsed.file === _const.SELECTED && req.method === "GET") { + _getSelectedCommand(req, res); + return; + } else if (req.urlParsed.file === _const.CLEAR && req.method === "POST") { + _postClearCommand(req, res); + return; + } else if (req.urlParsed.chunks[0] === _const.CSS && req.method === "GET") { + _getCssCommand(req, res); + return; + } else if (req.urlParsed.file === _const.TEXT && req.method === "GET") { + _getTextCommand(req, res); + return; + } else if (req.urlParsed.chunks[0] === _const.EQUALS && req.method === "GET") { + _getEqualsCommand(req, res); + return; + } else if (req.urlParsed.file === _const.LOCATION && req.method === "GET") { + _getLocationCommand(req, res); + return; + } else if (req.urlParsed.file === _const.LOCATION_IN_VIEW && req.method === "GET") { + _getLocationInViewCommand(req, res); + return; + } else if (req.urlParsed.file === _const.SIZE && req.method === "GET") { + _getSizeCommand(req, res); + return; + } else if (req.urlParsed.file === "" && req.method === "GET") { //< GET "/session/:id/element/:id" + // The response to this command is not defined in the specs: + // here we just return the Element JSON ID. + res.success(_session.getId(), _getJSON()); + return; + } // else ... + + throw _errors.createInvalidReqInvalidCommandMethodEH(req); + }, + + _getDisplayedCommand = function(req, res) { + var displayed = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("is_displayed"), + _getJSON()); + res.respondBasedOnResult(_session, req, displayed); + }, + + _getEnabledCommand = function(req, res) { + var enabled = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("is_enabled"), + _getJSON()); + res.respondBasedOnResult(_session, req, enabled); + }, + + _getLocationResult = function(req) { + return _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return (" + require("./webdriver_atoms.js").get("get_location") + ")(arguments[0]);", + [_getJSON()]); + }, + + _getLocation = function(req) { + var result = _getLocationResult(req); + + _log.debug("_getLocation", JSON.stringify(result)); + + if (result.status === 0) { + return result.value; + } else { + return null; + } + }, + + _getLocationCommand = function(req, res) { + var locationRes = _getLocationResult(req); + + _log.debug("_getLocationCommand", JSON.stringify(locationRes)); + + res.respondBasedOnResult(_session, req, locationRes); + }, + + _getLocationInViewResult = function (req) { + return _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return (" + require("./webdriver_atoms.js").get("get_location_in_view") + ")(arguments[0]);", + [_getJSON()]); + }, + + _getLocationInView = function (req) { + var result = _getLocationInViewResult(req); + + _log.debug("_getLocationInView", JSON.stringify(result)); + + if (result.status === 0) { + return result.value; + } else { + return null; + } + }, + + _getLocationInViewCommand = function (req, res) { + var locationInViewRes = _getLocationInViewResult(req); + + _log.debug("_getLocationInViewCommand", JSON.stringify(locationInViewRes)); + + // Something went wrong: report the error + res.respondBasedOnResult(_session, req, locationInViewRes); + }, + + _getSizeResult = function (req) { + return _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("get_size"), + _getJSON()); + }, + + _getSize = function (req) { + var result = JSON.parse(_getSizeResult(req)); + + _log.debug("_getSize", JSON.stringify(result)); + + if (result.status === 0) { + return result.value; + } else { + return null; + } + }, + + _getSizeCommand = function (req, res) { + var sizeRes = _getSizeResult(req); + + _log.debug("_getSizeCommand", JSON.stringify(sizeRes)); + + res.respondBasedOnResult(_session, req, sizeRes); + }, + + _normalizeSpecialChars = function(str) { + var resultStr = "", + i, ilen; + + for(i = 0, ilen = str.length; i < ilen; ++i) { + switch(str[i]) { + case '\b': + resultStr += '\uE003'; //< Backspace + break; + case '\t': + resultStr += '\uE004'; // Tab + break; + case '\r': + resultStr += '\uE006'; // Return + if (str.length > i+1 && str[i+1] === '\n') { //< Return on Windows + ++i; //< skip the next '\n' + } + break; + case '\n': + resultStr += '\uE007'; // Enter + break; + default: + resultStr += str[i]; + break; + } + } + + return resultStr; + }, + + _postValueCommand = function(req, res) { + var postObj = JSON.parse(req.post), + currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req), + typeRes, + text, + fsModule = require("fs"); + + // Ensure all required parameters are available + if (typeof(postObj) === "object" && typeof(postObj.value) === "object") { + // Normalize input: some binding might send an array of single characters + text = postObj.value.join(""); + + // Detect if it's an Input File type (that requires special behaviour), and the File actually exists + if (_getTagName(currWindow).toLowerCase() === "input" && + _getAttribute(currWindow, "type").toLowerCase() === "file" && + fsModule.exists(text)) { + // Register a one-shot-callback to fill the file picker once invoked by clicking on the element + currWindow.setOneShotCallback("onFilePicker", function(oldFile) { + // Send the response as soon as we are done setting the value in the "input[type=file]" element + setTimeout(function() { + res.respondBasedOnResult(_session, req, typeRes); + }, 1); + + return text; + }); + + // Click on the element! + typeRes = currWindow.evaluate(require("./webdriver_atoms.js").get("click"), _getJSON()); + } else { + // Normalize for special characters + text = _normalizeSpecialChars(text); + + // Execute the "type" atom on an empty string only to force focus to the element. + // TODO: This is a hack that needs to be corrected with a proper method to set focus. + typeRes = currWindow.evaluate(require("./webdriver_atoms.js").get("type"), _getJSON(), ""); + + // Send keys to the page, using Native Events + _session.inputs.sendKeys(_session, text); + + // Only clear the modifier keys if this was called using element.sendKeys(). + // Calling this from the Advanced Interactions API doesn't clear the modifier keys. + if (req.urlParsed.file === _const.VALUE) { + _session.inputs.clearModifierKeys(_session); + } + + currWindow.waitIfLoading(function() { + // Return the result of this typing + res.respondBasedOnResult(_session, req, typeRes); + }); + } + return; + } + + throw _errors.createInvalidReqMissingCommandParameterEH(req); + }, + + _getNameCommand = function(req, res) { + var result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return arguments[0].tagName;", + [_getJSON()]); + + // Convert value to a lowercase string as per WebDriver JSONWireProtocol spec + // @see http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/name + if(result.status === 0) { + result.value = result.value.toLowerCase(); + } + + res.respondBasedOnResult(_session, req, result); + }, + + _getAttributeCommand = function(req, res) { + var attributeValueAtom = require("./webdriver_atoms.js").get("get_attribute_value"), + result; + + if (typeof(req.urlParsed.file) === "string" && req.urlParsed.file.length > 0) { + // Read the attribute + result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + attributeValueAtom, // < Atom to read an attribute + _getJSON(), // < Element to read from + req.urlParsed.file); // < Attribute to read + + res.respondBasedOnResult(_session, req, result); + return; + } + + throw _errors.createInvalidReqMissingCommandParameterEH(req); + }, + + _getTextCommand = function(req, res) { + var result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("get_text"), + _getJSON()); + res.respondBasedOnResult(_session, req, result); + }, + + _getEqualsCommand = function(req, res) { + var result; + + if (typeof(req.urlParsed.file) === "string" && req.urlParsed.file.length > 0) { + result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return arguments[0].isSameNode(arguments[1]);", + [_getJSON(), _getJSON(req.urlParsed.file)]); + + res.respondBasedOnResult(_session, req, result); + return; + } + + throw _errors.createInvalidReqMissingCommandParameterEH(req); + }, + + _postSubmitCommand = function(req, res) { + var currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req), + submitRes, + abortCallback = false; + + currWindow.execFuncAndWaitForLoad(function() { + // do the submit + submitRes = currWindow.evaluate(require("./webdriver_atoms.js").get("submit"), _getJSON()); + + // If Submit was NOT positive, status will be set to something else than '0' + submitRes = JSON.parse(submitRes); + if (submitRes && submitRes.status !== 0) { + abortCallback = true; //< handling the error here + res.respondBasedOnResult(_session, req, submitRes); + } + }, + function(status) { //< onLoadFinished + // Report about the Load, only if it was not already handled + if (!abortCallback) { + res.success(_session.getId()); + } + }, + function(errMsg) { + var errCode = errMsg === "timeout" + ? _errors.FAILED_CMD_STATUS_CODES.Timeout + : _errors.FAILED_CMD_STATUS_CODES.UnknownError; + + // Report Submit Error, only if callbacks were not "aborted" + if (!abortCallback) { + _errors.handleFailedCommandEH(errCode, "Submit failed: " + errMsg, req, res, _session); + } + }); + }, + + _postClickCommand = function(req, res) { + var currWindow = _protoParent.getSessionCurrWindow.call(this, _session, req), + clickRes, + abortCallback = false; + + // Clicking on Current Element can cause a page load, hence we need to wait for it to happen + currWindow.execFuncAndWaitForLoad(function() { + // do the click + clickRes = currWindow.evaluate(require("./webdriver_atoms.js").get("click"), _getJSON()); + + // If Click was NOT positive, status will be set to something else than '0' + clickRes = JSON.parse(clickRes); + if (clickRes && clickRes.status !== 0) { + abortCallback = true; //< handling the error here + res.respondBasedOnResult(_session, req, clickRes); + } + }, + function(status) { //< onLoadFinished + // Report Load Finished, only if callbacks were not "aborted" + if (!abortCallback) { + res.success(_session.getId()); + } + }, + function(errMsg) { + var errCode = errMsg === "timeout" + ? _errors.FAILED_CMD_STATUS_CODES.Timeout + : _errors.FAILED_CMD_STATUS_CODES.UnknownError; + + // Report Load Error, only if callbacks were not "aborted" + if (!abortCallback) { + _errors.handleFailedCommandEH(errCode, "Click failed: " + errMsg, req, res, _session); + } + }); + }, + + _getSelectedCommand = function(req, res) { + var result = JSON.parse(_protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("is_selected"), + _getJSON())); + + res.respondBasedOnResult(_session, req, result); + }, + + _postClearCommand = function(req, res) { + var result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("clear"), + _getJSON()); + res.respondBasedOnResult(_session, req, result); + }, + + _getCssCommand = function(req, res) { + var cssPropertyName = req.urlParsed.file, + result; + + // Check that a property name was indeed provided + if (typeof(cssPropertyName) === "string" || cssPropertyName.length > 0) { + result = _protoParent.getSessionCurrWindow.call(this, _session, req).evaluate( + require("./webdriver_atoms.js").get("get_value_of_css_property"), + _getJSON(), + cssPropertyName); + + res.respondBasedOnResult(_session, req, result); + return; + } + + throw _errors.createInvalidReqMissingCommandParameterEH(req); + }, + + _getAttribute = function(currWindow, attributeName) { + var attributeValueAtom = require("./webdriver_atoms.js").get("get_attribute_value"), + result = currWindow.evaluate( + attributeValueAtom, // < Atom to read an attribute + _getJSON(), // < Element to read from + attributeName); // < Attribute to read + + return JSON.parse(result).value; + }, + + _getTagName = function(currWindow) { + var result = currWindow.evaluate( + require("./webdriver_atoms.js").get("execute_script"), + "return arguments[0].tagName;", + [_getJSON()]); + + return result.value; + }, + + /** + * This method can generate any Element JSON: just provide an ID. + * Will return the one of the current Element if no ID is provided. + * @param elementId ID of the Element to describe in JSON format, + * or undefined to get the one fo the current Element. + */ + _getJSON = function(elementId) { + return { + "ELEMENT" : elementId || _getId() + }; + }, + + _getId = function() { + return _id; + }, + _getSession = function() { + return _session; + }; + + // public: + return { + handle : _handle, + getId : _getId, + getJSON : _getJSON, + getSession : _getSession, + postValueCommand : _postValueCommand, + getLocation : _getLocation, + getLocationInView: _getLocationInView, + getSize: _getSize + }; +}; +// prototype inheritance: +ghostdriver.WebElementReqHand.prototype = new ghostdriver.RequestHandler(); diff --git a/src/ghostdriver/session.js b/src/ghostdriver/session.js new file mode 100644 index 0000000..bc2be18 --- /dev/null +++ b/src/ghostdriver/session.js @@ -0,0 +1,738 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.Session = function(desiredCapabilities) { + // private: + const + _const = { + TIMEOUT_NAMES : { + SCRIPT : "script", + IMPLICIT : "implicit", + PAGE_LOAD : "page load" + }, + ONE_SHOT_POSTFIX : "OneShot", + LOG_TYPES : { + HAR : "har", + BROWSER : "browser" + }, + PROXY_TYPES : { + MANUAL : "manual", + DIRECT : "direct" + } + }; + + var + _defaultCapabilities = { // TODO - Actually try to match the "desiredCapabilities" instead of ignoring them + "browserName" : "phantomjs", + "version" : phantom.version.major + '.' + phantom.version.minor + '.' + phantom.version.patch, + "driverName" : "ghostdriver", + "driverVersion" : ghostdriver.version, + "platform" : ghostdriver.system.os.name + '-' + ghostdriver.system.os.version + '-' + ghostdriver.system.os.architecture, + "javascriptEnabled" : true, + "takesScreenshot" : true, + "handlesAlerts" : false, //< TODO + "databaseEnabled" : false, //< TODO + "locationContextEnabled" : false, //< TODO Target is 1.1 + "applicationCacheEnabled" : false, //< TODO Support for AppCache (?) + "browserConnectionEnabled" : false, //< TODO + "cssSelectorsEnabled" : true, + "webStorageEnabled" : false, //< TODO support for LocalStorage/SessionStorage + "rotatable" : false, //< TODO Target is 1.1 + "acceptSslCerts" : false, //< TODO + "nativeEvents" : true, //< TODO Only some commands are Native Events currently + "proxy" : { //< TODO Support more proxy options - PhantomJS does allow setting from command line + "proxyType" : _const.PROXY_TYPES.DIRECT + }, + }, + _negotiatedCapabilities = { + "browserName" : _defaultCapabilities.browserName, + "version" : _defaultCapabilities.version, + "driverName" : _defaultCapabilities.driverName, + "driverVersion" : _defaultCapabilities.driverVersion, + "platform" : _defaultCapabilities.platform, + "javascriptEnabled" : _defaultCapabilities.javascriptEnabled, + "takesScreenshot" : typeof(desiredCapabilities.takesScreenshot) === "undefined" ? + _defaultCapabilities.takesScreenshot : + desiredCapabilities.takesScreenshot, + "handlesAlerts" : _defaultCapabilities.handlesAlerts, + "databaseEnabled" : _defaultCapabilities.databaseEnabled, + "locationContextEnabled" : _defaultCapabilities.locationContextEnabled, + "applicationCacheEnabled" : _defaultCapabilities.applicationCacheEnabled, + "browserConnectionEnabled" : _defaultCapabilities.browserConnectionEnabled, + "cssSelectorsEnabled" : _defaultCapabilities.cssSelectorsEnabled, + "webStorageEnabled" : _defaultCapabilities.webStorageEnabled, + "rotatable" : _defaultCapabilities.rotatable, + "acceptSslCerts" : _defaultCapabilities.acceptSslCerts, + "nativeEvents" : _defaultCapabilities.nativeEvents, + "proxy" : typeof(desiredCapabilities.proxy) === "undefined" ? + _defaultCapabilities.proxy : + desiredCapabilities.proxy + }, + // NOTE: This value is needed for Timeouts Upper-bound limit. + // "setTimeout/setInterval" accept only 32 bit integers, even though Number are all Doubles (go figure!) + // Interesting details here: {@link http://stackoverflow.com/a/4995054}. + _max32bitInt = Math.pow(2, 31) -1, //< Max 32bit Int + _timeouts = { + "script" : _max32bitInt, + "implicit" : 200, //< 200ms + "page load" : _max32bitInt, + }, + _windows = {}, //< NOTE: windows are "webpage" in Phantom-dialect + _currentWindowHandle = null, + _cookieJar = require('cookiejar').create(), + _id = require("./third_party/uuid.js").v1(), + _inputs = ghostdriver.Inputs(), + _capsPageSettingsPref = "phantomjs.page.settings.", + _capsPageCustomHeadersPref = "phantomjs.page.customHeaders.", + _capsPageSettingsProxyPref = "proxy", + _pageSettings = {}, + _additionalPageSettings = { + userName: null, + password: null + }, + _pageCustomHeaders = {}, + _log = ghostdriver.logger.create("Session [" + _id + "]"), + k, settingKey, headerKey, proxySettings; + + var + /** + * Parses proxy JSON object and return proxy settings for phantom + * + * @param proxyCapability proxy JSON Object: @see https://code.google.com/p/selenium/wiki/DesiredCapabilities + */ + _getProxySettingsFromCapabilities = function(proxyCapability) { + var proxySettings = {}; + if (proxyCapability["proxyType"].toLowerCase() == _const.PROXY_TYPES.MANUAL) { //< TODO: support other options + if (proxyCapability["httpProxy"] !== "null") { //< TODO: support other proxy types + var urlParts = proxyCapability["httpProxy"].split(':'); + proxySettings["ip"] = urlParts[0]; + proxySettings["port"] = urlParts[1]; + proxySettings["proxyType"] = "http"; + proxySettings["user"] = ""; + proxySettings["password"] = ""; + + return proxySettings; + } + } + return proxySettings; + }; + + // Searching for `phantomjs.settings.* and phantomjs.customHeaders.*` in the Desired Capabilities and merging with the Negotiated Capabilities + // Possible values for settings: @see https://github.com/ariya/phantomjs/wiki/API-Reference#wiki-webpage-settings. + // Possible values for customHeaders: @see https://github.com/ariya/phantomjs/wiki/API-Reference-WebPage#wiki-webpage-customHeaders. + for (k in desiredCapabilities) { + if (k.indexOf(_capsPageSettingsPref) === 0) { + settingKey = k.substring(_capsPageSettingsPref.length); + if (settingKey.length > 0) { + _negotiatedCapabilities[k] = desiredCapabilities[k]; + _pageSettings[settingKey] = desiredCapabilities[k]; + } + } + if (k.indexOf(_capsPageCustomHeadersPref) === 0) { + headerKey = k.substring(_capsPageCustomHeadersPref.length); + if (headerKey.length > 0) { + _negotiatedCapabilities[k] = desiredCapabilities[k]; + _pageCustomHeaders[headerKey] = desiredCapabilities[k]; + } + } + if (k.indexOf(_capsPageSettingsProxyPref) === 0) { + proxySettings = _getProxySettingsFromCapabilities(desiredCapabilities[k]); + phantom.setProxy(proxySettings["ip"], proxySettings["port"], proxySettings["proxyType"], proxySettings["user"], proxySettings["password"]); + } + } + + var + /** + * Executes a function and waits for Load to happen. + * + * @param code Code to execute: a Function or just plain code + * @param onLoadFunc Function to execute when page finishes Loading + * @param onErrorFunc Function to execute in case of error + * (eg. Javascript error, page load problem or timeout). + * @param execTypeOpt Decides if to "apply" the function directly or page."eval" it. + * Optional. Default value is "apply". + */ + _execFuncAndWaitForLoadDecorator = function(code, onLoadFunc, onErrorFunc, execTypeOpt) { + // convert 'arguments' to a real Array + var args = Array.prototype.splice.call(arguments, 0), + thisPage = this, + onLoadFinishedArgs = null, + onErrorArgs = null; + + // Normalize "execTypeOpt" value + if (typeof(execTypeOpt) === "undefined" || + (execTypeOpt !== "apply" && execTypeOpt !== "eval")) { + execTypeOpt = "apply"; + } + + // Register Callbacks to grab any async event we are interested in + this.setOneShotCallback("onLoadFinished", function (status) { + _log.debug("_execFuncAndWaitForLoadDecorator", "onLoadFinished: " + status); + + onLoadFinishedArgs = Array.prototype.slice.call(arguments); + }); + + // Execute "code" + if (execTypeOpt === "eval") { + // Remove arguments used by this function before providing them to the target code. + // NOTE: Passing 'code' (to evaluate) and '0' (timeout) to 'evaluateAsync'. + args.splice(0, 3, code, 0); + // Invoke the Page Eval with the provided function + this.evaluateAsync.apply(this, args); + } else { + // Remove arguments used by this function before providing them to the target function. + args.splice(0, 3); + // "Apply" the provided function + code.apply(this, args); + } + + // Wait 10ms before proceeding any further: in this window of time + // the page can react and start loading (if it has to). + setTimeout(function() { + var loadingStartedTs = new Date().getTime(), + checkLoadingFinished; + + checkLoadingFinished = function() { + if (!_isLoading()) { //< page finished loading + _log.debug("_execFuncAndWaitForLoadDecorator", "Page Loading in Session: false"); + + if (onLoadFinishedArgs !== null) { + // Report the result of the "Load Finished" event + onLoadFunc.apply(thisPage, onLoadFinishedArgs); + } else { + // No page load was caused: just report "success" + onLoadFunc.call(thisPage, "success"); + } + + return; + } // else: + _log.debug("_execFuncAndWaitForLoadDecorator", "Page Loading in Session: true"); + + // Timeout error? + if (new Date().getTime() - loadingStartedTs > _getPageLoadTimeout()) { + // Report the "Timeout" event + onErrorFunc.call(thisPage, "timeout"); + return; + } + + // Retry in 100ms + setTimeout(checkLoadingFinished, 100); + }; + checkLoadingFinished(); + }, 10); //< 10ms + }, + + /** + * Wait for Page to be done Loading before executing of callback. + * Also, it considers "Page Timeout" to avoid waiting indefinitely. + * NOTE: This is useful for cases where it's not certain a certain action + * just executed MIGHT cause a page to start loading. + * It's a "best effort" approach and the user is given the use of + * "Page Timeout" to tune to their needs. + * + * @param callback Function to execute when done or timed out + */ + _waitIfLoadingDecorator = function(callback) { + var thisPage = this, + waitStartedTs = new Date().getTime(), + checkDoneLoading; + + checkDoneLoading = function() { + if (!_isLoading() //< Session is not loading (any more?) + || (new Date().getTime() - waitStartedTs > _getPageLoadTimeout())) { //< OR Page Timeout expired + callback.call(thisPage); + return; + } + + _log.debug("_waitIfLoading", "Still loading (wait using Implicit Timeout)"); + + // Retry in 10ms + setTimeout(checkDoneLoading, 10); + }; + checkDoneLoading(); + }, + + _oneShotCallbackFactory = function(page, callbackName) { + return function() { + var oneShotCallbackName = callbackName + _const.ONE_SHOT_POSTFIX, + i, retVal; + + try { + // If there are callback functions registered + if (page[oneShotCallbackName] instanceof Array + && page[oneShotCallbackName].length > 0) { + _log.debug("_oneShotCallback", callbackName); + + // Invoke all the callback functions (once) + for (i = page[oneShotCallbackName].length -1; i >= 0; --i) { + retVal = page[oneShotCallbackName][i].apply(page, arguments); + } + + // Remove all the callback functions now + page[oneShotCallbackName] = []; + } + } catch (e) { + // In case the "page" object has been closed, + // the code above will fail: that's OK. + } + + // Return (latest) value + return retVal; + }; + }, + + _setOneShotCallbackDecorator = function(callbackName, handlerFunc) { + var oneShotCallbackName = callbackName + _const.ONE_SHOT_POSTFIX; + + // Initialize array of One Shot Callbacks + if (!(this[oneShotCallbackName] instanceof Array)) { + this[oneShotCallbackName] = []; + } + this[oneShotCallbackName].push(handlerFunc); + }, + + // Add any new page to the "_windows" container of this session + _addNewPage = function(newPage) { + _log.debug("_addNewPage"); + + // decorate the new Window/Page + newPage = _decorateNewWindow(newPage); + // set session-specific CookieJar + newPage.cookieJar = _cookieJar; + // store the Window/Page by WindowHandle + _windows[newPage.windowHandle] = newPage; + }, + + // Delete any closing page from the "_windows" container of this session + _deleteClosingPage = function(closingPage) { + _log.debug("_deleteClosingPage"); + + // Need to be defensive, as the "closing" can be cause by Client Commands + if (_windows.hasOwnProperty(closingPage.windowHandle)) { + delete _windows[closingPage.windowHandle]; + } + }, + + _decorateNewWindow = function(page) { + var k; + + // Decorating: + // 0. Pages lifetime will be managed by Driver, not the pages + page.ownsPages = false; + + // 1. Random Window Handle + page.windowHandle = require("./third_party/uuid.js").v1(); + + // 2. Initialize the One-Shot Callbacks + page["onLoadStarted"] = _oneShotCallbackFactory(page, "onLoadStarted"); + page["onLoadFinished"] = _oneShotCallbackFactory(page, "onLoadFinished"); + page["onUrlChanged"] = _oneShotCallbackFactory(page, "onUrlChanged"); + page["onFilePicker"] = _oneShotCallbackFactory(page, "onFilePicker"); + page["onCallback"] = _oneShotCallbackFactory(page, "onCallback"); + + // 3. Utility methods + page.execFuncAndWaitForLoad = _execFuncAndWaitForLoadDecorator; + page.setOneShotCallback = _setOneShotCallbackDecorator; + page.waitIfLoading = _waitIfLoadingDecorator; + + // 4. Store every newly created page + page.onPageCreated = _addNewPage; + + // 5. Remove every closing page + page.onClosing = _deleteClosingPage; + + // 6. Applying Page settings received via capabilities + for (k in _pageSettings) { + // Apply setting only if really supported by PhantomJS + if (page.settings.hasOwnProperty(k) || _additionalPageSettings.hasOwnProperty(k)) { + page.settings[k] = _pageSettings[k]; + } + } + + // 7. Applying Page custom headers received via capabilities + page.customHeaders = _pageCustomHeaders; + + // 8. Log Page internal errors + page.onError = function(errorMsg, errorStack) { + var stack = ''; + + // Prep the "stack" part of the message + errorStack.forEach(function (stackEntry, idx, arr) { + stack += " " //< a bit of indentation + + (stackEntry.function || "(anonymous function)") + + " (" + stackEntry.file + ":" + stackEntry.line + ")"; + stack += idx < arr.length - 1 ? "\n" : ""; + }); + + // Log as error + _log.error("page.onError", "msg: " + errorMsg); + _log.error("page.onError", "stack:\n" + stack); + + // Register as part of the "browser" log + page.browserLog.push(_createLogEntry("WARNING", errorMsg + "\n" + stack)); + }; + + // 9. Log Page console messages + page.browserLog = []; + page.onConsoleMessage = function(msg, lineNum, sourceId) { + // Log as debug + _log.debug("page.onConsoleMessage", msg); + + // Register as part of the "browser" log + page.browserLog.push(_createLogEntry("INFO", msg + " (" + sourceId + ":" + lineNum + ")")); + }; + + // 10. Log Page network activity + page.resources = []; + page.startTime = null; + page.endTime = null; + page.setOneShotCallback("onLoadStarted", function() { + page.startTime = new Date(); + }); + page.setOneShotCallback("onLoadFinished", function() { + page.endTime = new Date(); + }); + page.onResourceRequested = function (req) { + _log.debug("page.onResourceRequested", JSON.stringify(req)); + + // Register HTTP Request + page.resources[req.id] = { + request: req, + startReply: null, + endReply: null, + error: null + }; + }; + page.onResourceReceived = function (res) { + _log.debug("page.onResourceReceived", JSON.stringify(res)); + + // Register HTTP Response + page.resources[res.id] || (page.resources[res.id] = {}); + if (res.stage === 'start') { + page.resources[res.id].startReply = res; + } else if (res.stage === 'end') { + page.resources[res.id].endReply = res; + } + }; + page.onResourceError = function(resError) { + _log.debug("page.onResourceError", JSON.stringify(resError)); + + // Register HTTP Error + page.resources[resError.id] || (page.resources[resError.id] = {}); + page.resources[resError.id].error = resError; + }; + page.onResourceTimeout = function(req) { + _log.debug("page.onResourceTimeout", JSON.stringify(req)); + + // Register HTTP Timeout + page.resources[req.id] || (page.resources[req.id] = {}); + page.resources[req.id].error = req; + }; + page.onNavigationRequested = function(url, type, willNavigate, main) { + // Clear page log before page loading + if (main && willNavigate) { + _clearPageLog(page); + } + }; + + _log.info("page.settings", JSON.stringify(page.settings)); + _log.info("page.customHeaders: ", JSON.stringify(page.customHeaders)); + + return page; + }, + + _createLogEntry = function(level, message) { + return { + "level" : level, + "message" : message, + "timestamp" : (new Date()).getTime() + }; + }, + + /** + * Is any window in this Session Loading? + * @returns "true" if at least 1 window is loading. + */ + _isLoading = function() { + var wHandle; + + for (wHandle in _windows) { + if (_windows[wHandle].loading) { + return true; + } + } + + // If we arrived here, means that no window is loading + return false; + }, + + /** + * According to log method specification we have to clear log after each page refresh. + * https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/log + * @param {Object} page + * @private + */ + _clearPageLog = function (page) { + page.resources = []; + page.browserLog = []; + }, + + _getWindow = function(handleOrName) { + var page = null, + k; + + if (_isValidWindowHandle(handleOrName)) { + // Search by "handle" + page = _windows[handleOrName]; + } else { + // Search by "name" + for (k in _windows) { + if (_windows[k].windowName === handleOrName) { + page = _windows[k]; + break; + } + } + } + + return page; + }, + + _init = function() { + var page; + + // Ensure a Current Window is available, if it's found to be `null` + if (_currentWindowHandle === null) { + // Create the first Window/Page + page = require("webpage").create(); + // Decorate it with listeners and helpers + page = _decorateNewWindow(page); + // set session-specific CookieJar + page.cookieJar = _cookieJar; + // Make the new Window, the Current Window + _currentWindowHandle = page.windowHandle; + // Store by WindowHandle + _windows[_currentWindowHandle] = page; + } + }, + + _getCurrentWindow = function() { + var page = null; + + if (_windows.hasOwnProperty(_currentWindowHandle)) { + page = _windows[_currentWindowHandle]; + } + + // TODO Handle "null" cases throwing a "no such window" error + + return page; + }, + + _switchToWindow = function(handleOrName) { + var page = _getWindow(handleOrName); + + if (page !== null) { + // Switch current window and return "true" + _currentWindowHandle = page.windowHandle; + // Switch to the Main Frame of that window + page.switchToMainFrame(); + return true; + } + + // Couldn't find the window, so return "false" + return false; + }, + + _closeCurrentWindow = function() { + if (_currentWindowHandle !== null) { + return _closeWindow(_currentWindowHandle); + } + return false; + }, + + _closeWindow = function(handleOrName) { + var page = _getWindow(handleOrName), + handle; + + if (page !== null) { + handle = page.windowHandle; + _windows[handle].close(); + delete _windows[handle]; + return true; + } + return false; + }, + + _getWindowsCount = function() { + return Object.keys(_windows).length; + }, + + _getCurrentWindowHandle = function() { + if (!_isValidWindowHandle(_currentWindowHandle)) { + return null; + } + return _currentWindowHandle; + }, + + _isValidWindowHandle = function(handle) { + return _windows.hasOwnProperty(handle); + }, + + _getWindowHandles = function() { + return Object.keys(_windows); + }, + + _setTimeout = function(type, ms) { + // In case the chosen timeout is less than 0, we reset it to `_max32bitInt` + if (ms < 0) { + _timeouts[type] = _max32bitInt; + } else { + _timeouts[type] = ms; + } + }, + + _getTimeout = function(type) { + return _timeouts[type]; + }, + + _getScriptTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.SCRIPT); + }, + + _getImplicitTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.IMPLICIT); + }, + + _getPageLoadTimeout = function() { + return _getTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD); + }, + + _setScriptTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.SCRIPT, ms); + }, + + _setImplicitTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.IMPLICIT, ms); + }, + + _setPageLoadTimeout = function(ms) { + _setTimeout(_const.TIMEOUT_NAMES.PAGE_LOAD, ms); + }, + + _executePhantomJS = function(page, script, args) { + try { + var code = new Function(script); + return code.apply(page, args); + } catch (e) { + return e; + } + }, + + _aboutToDelete = function() { + var k; + + // Close current window first + _closeCurrentWindow(); + + // Releasing page resources and deleting the objects + for (k in _windows) { + _closeWindow(k); + } + + // Release CookieJar resources + _cookieJar.close(); + }, + + _getLog = function (type) { + var har = require('./third_party/har.js'), + page, tmp; + + // Return "HAR" as Log Type "har" + if (type === _const.LOG_TYPES.HAR) { + page = _getCurrentWindow(); + tmp = []; + tmp.push(_createLogEntry( + "INFO", + JSON.stringify(har.createHar(page, page.resources)))); + return tmp; + } + + // Return Browser Console Log + if (type === _const.LOG_TYPES.BROWSER) { + return _getCurrentWindow().browserLog; + } + + // Return empty Log + return []; + }, + + _getLogTypes = function () { + var logTypes = [], k; + + for (k in _const.LOG_TYPES) { + logTypes.push(_const.LOG_TYPES[k]); + } + + return logTypes; + }; + + // Initialize the Session. + // Particularly, create the first empty page/window. + _init(); + + _log.debug("Session.desiredCapabilities", JSON.stringify(desiredCapabilities)); + _log.info("Session.negotiatedCapabilities", JSON.stringify(_negotiatedCapabilities)); + + // public: + return { + getCapabilities : function() { return _negotiatedCapabilities; }, + getId : function() { return _id; }, + switchToWindow : _switchToWindow, + getCurrentWindow : _getCurrentWindow, + closeCurrentWindow : _closeCurrentWindow, + getWindow : _getWindow, + closeWindow : _closeWindow, + getWindowsCount : _getWindowsCount, + getCurrentWindowHandle : _getCurrentWindowHandle, + getWindowHandles : _getWindowHandles, + isValidWindowHandle : _isValidWindowHandle, + aboutToDelete : _aboutToDelete, + inputs : _inputs, + setScriptTimeout : _setScriptTimeout, + setImplicitTimeout : _setImplicitTimeout, + setPageLoadTimeout : _setPageLoadTimeout, + getScriptTimeout : _getScriptTimeout, + getImplicitTimeout : _getImplicitTimeout, + getPageLoadTimeout : _getPageLoadTimeout, + executePhantomJS : _executePhantomJS, + timeoutNames : _const.TIMEOUT_NAMES, + isLoading : _isLoading, + getLog: _getLog, + getLogTypes: _getLogTypes + }; +}; diff --git a/src/ghostdriver/third_party/console++.js b/src/ghostdriver/third_party/console++.js new file mode 100755 index 0000000..3a854ad --- /dev/null +++ b/src/ghostdriver/third_party/console++.js @@ -0,0 +1,290 @@ +/* +This file is part of the Console++ by Ivan De Marino . + +Copyright (c) 2012-2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +if (console.LEVELS) { + // Already loaded. No need to manipulate the "console" further. + // NOTE: NodeJS already caches modules. This is just defensive coding. + exports = console; + return; +} + +// private: +var _ANSICODES = { + 'reset' : '\033[0m', + 'bold' : '\033[1m', + 'italic' : '\033[3m', + 'underline' : '\033[4m', + 'blink' : '\033[5m', + 'black' : '\033[30m', + 'red' : '\033[31m', + 'green' : '\033[32m', + 'yellow' : '\033[33m', + 'blue' : '\033[34m', + 'magenta' : '\033[35m', + 'cyan' : '\033[36m', + 'white' : '\033[37m' + }, + _LEVELS = { + NONE : 0, + OFF : 0, //< alias for "NONE" + ERROR : 1, + WARN : 2, + WARNING : 2, //< alias for "WARN" + INFO : 3, + INFORMATION : 3, //< alias for "INFO" + DEBUG : 4 + }, + _LEVELS_COLOR = [ //< _LEVELS_COLOR position matches the _LEVELS values + "red", + "yellow", + "cyan", + "green" + ], + _LEVELS_NAME = [ //< _LEVELS_NAME position matches the _LEVELS values + "NONE", + "ERROR", + "WARN ", + "INFO ", + "DEBUG" + ], + _console = { + error : console.error, + warn : console.warn, + info : console.info, + debug : console.log, + log : console.log + }, + _level = _LEVELS.DEBUG, + _colored = true, + _messageColored = false, + _timed = true, + _onOutput = null; + +/** + * Take a string and apply console ANSI colors for expressions "#color{msg}" + * NOTE: Does nothing if "console.colored === false". + * + * @param str Input String + * @returns Same string but with colors applied + */ +var _applyColors = function(str) { + var tag = /#([a-z]+)\{|\}/, + cstack = [], + matches = null, + orig = null, + name = null, + code = null; + + while (tag.test(str)) { + matches = tag.exec(str); + orig = matches[0]; + + if (console.isColored()) { + if (orig === '}') { + cstack.pop(); + } else { + name = matches[1]; + if (name in _ANSICODES) { + code = _ANSICODES[name]; + cstack.push(code); + } + } + + str = str.replace(orig, _ANSICODES.reset + cstack.join('')); + } else { + str = str.replace(orig, ''); + } + } + return str; +}; + +/** + * Decorate the Arguments passed to the console methods we override. + * First element, the message, is now colored, timed and more (based on config). + * + * @param argsArray Array of arguments to decorate + * @param level Logging level to apply (regulates coloring and text) + * @returns Array of Arguments, decorated. + */ +var _decorateArgs = function(argsArray, level) { + var args = Array.prototype.slice.call(argsArray, 1), + msg = argsArray[0], + levelMsg; + + if (console.isColored()) { + levelMsg = _applyColors("#" + console.getLevelColor(level) + "{" + console.getLevelName(level) + "}"); + msg = _applyColors(msg); + + if (console.isMessageColored()) { + msg = _applyColors("#" + console.getLevelColor(level) + "{" + msg + "}"); + } + } else { + levelMsg = console.getLevelName(level); + } + + msg = _formatMessage(msg, levelMsg); + + args.splice(0, 0, msg); + + return args; +}; + +/** + * Formats the Message content. + * @param msg The message itself + * @param levelMsg The portion of message that contains the Level (maybe colored) + * @retuns The formatted message + */ +var _formatMessage = function(msg, levelMsg) { + if (console.isTimestamped()) { + return "[" + levelMsg + " - " + new Date().toJSON() + "] " + msg; + } else { + return "[" + levelMsg + "] " + msg; + } +}; + +/** + * Invokes the "console.onOutput()" callback, if it was set by user. + * This is useful in case the user wants to write the console output to another media as well. + * + * The callback is invoked with 2 parameters: + * - formattedMessage: formatted message, ready for output + * - levelName: the name of the logging level, to inform the user + * + * @param msg The Message itself + * @param level The Message Level (Number) + */ +var _invokeOnOutput = function(msg, level) { + var formattedMessage, + levelName; + + if (_onOutput !== null && typeof(_onOutput) === "function") { + levelName = console.getLevelName(level); + formattedMessage = _formatMessage(msg, levelName); + + _onOutput.call(null, formattedMessage, levelName); + } +}; + + +// public: +// CONSTANT: Logging Levels +console.LEVELS = _LEVELS; + +// Set/Get Level +console.setLevel = function(level) { + _level = level; +}; +console.getLevel = function() { + return _level; +}; +console.getLevelName = function(level) { + return _LEVELS_NAME[typeof(level) === "undefined" ? _level : level]; +}; +console.getLevelColor = function(level) { + return _LEVELS_COLOR[typeof(level) === "undefined" ? _level : level]; +}; +console.isLevelVisible = function(levelToCompare) { + return _level >= levelToCompare; +}; + +// Enable/Disable Colored Output +console.enableColor = function() { + _colored = true; +}; +console.disableColor = function() { + _colored = false; +}; +console.isColored = function() { + return _colored; +}; + +// Enable/Disable Colored Message Output +console.enableMessageColor = function() { + _messageColored = true; +}; +console.disableMessageColor = function() { + _messageColored = false; +}; +console.isMessageColored = function() { + return _messageColored; +}; + +// Enable/Disable Timestamped Output +console.enableTimestamp = function() { + _timed = true; +}; +console.disableTimestamp = function() { + _timed = false; +}; +console.isTimestamped = function() { + return _timed; +}; + +// Set OnOutput Callback (useful to write to file or something) +// Callback: `function(formattedMessage, levelName)` +console.onOutput = function(callback) { + _onOutput = callback; +}; + +// Decodes coloring markup in string +console.str2clr = function(str) { + return console.isColored() ? _applyColors(str): str; +}; + +// Overrides some key "console" Object methods +console.error = function(msg) { + if (arguments.length > 0 && this.isLevelVisible(_LEVELS.ERROR)) { + _console.error.apply(this, _decorateArgs(arguments, _LEVELS.ERROR)); + _invokeOnOutput(msg, _LEVELS.ERROR); + } +}; +console.warn = function(msg) { + if (arguments.length > 0 && this.isLevelVisible(_LEVELS.WARN)) { + _console.warn.apply(this, _decorateArgs(arguments, _LEVELS.WARN)); + _invokeOnOutput(msg, _LEVELS.WARN); + } +}; +console.info = function(msg) { + if (arguments.length > 0 && this.isLevelVisible(_LEVELS.INFO)) { + _console.info.apply(this, _decorateArgs(arguments, _LEVELS.INFO)); + _invokeOnOutput(msg, _LEVELS.INFO); + } +}; +console.debug = function(msg) { + if (arguments.length > 0 && this.isLevelVisible(_LEVELS.DEBUG)) { + _console.debug.apply(this, _decorateArgs(arguments, _LEVELS.DEBUG)); + _invokeOnOutput(msg, _LEVELS.DEBUG); + } +}; +console.log = function(msg) { + if (arguments.length > 0) { + _console.log.apply(this, arguments); + } +}; + +exports = console; diff --git a/src/ghostdriver/third_party/har.js b/src/ghostdriver/third_party/har.js new file mode 100644 index 0000000..232c8b8 --- /dev/null +++ b/src/ghostdriver/third_party/har.js @@ -0,0 +1,173 @@ +/** + * Page object + * @typedef {Object} PageObject + * @property {String} title - contents of tag + * @property {String} url - page URL + * @property {Date} startTime - time when page starts loading + * @property {Date} endTime - time when onLoad event fires + */ + +/** + * Resource object + * @typedef {Object} ResourceObject + * @property {Object} request - PhantomJS request object + * @property {Object} startReply - PhantomJS response object + * @property {Object} endReply - PhantomJS response object + */ + +/** + * This function is based on PhantomJS network logging example: + * https://github.com/ariya/phantomjs/blob/master/examples/netsniff.js + * + * @param {PageObject} page + * @param {ResourceObject} resources + * @returns {{log: {version: string, creator: {name: string, version: string}, pages: Array, entries: Array}}} + */ +exports.createHar = function (page, resources) { + var entries = []; + + resources.forEach(function (resource) { + var request = resource.request, + startReply = resource.startReply, + endReply = resource.endReply, + error = resource.error; + + if (!request) { + return; + } + + // Exclude Data URI from HAR file because + // they aren't included in specification + if (request.url.match(/(^data:image\/.*)/i)) { + return; + } + + if (error) { + // according to http://qt-project.org/doc/qt-4.8/qnetworkreply.html + switch (error.errorCode) { + case 1: + error.errorString = '(refused)'; + break; + case 2: + error.errorString = '(closed)'; + break; + case 3: + error.errorString = '(host not found)'; + break; + case 4: + error.errorString = '(timeout)'; + break; + case 5: + error.errorString = '(canceled)'; + break; + case 6: + error.errorString = '(ssl failure)'; + break; + case 7: + error.errorString = '(net failure)'; + break; + } + } + + if (startReply && endReply) { + entries.push({ + startedDateTime: request.time.toISOString(), + time: endReply.time - request.time, + request: { + method: request.method, + url: request.url, + httpVersion: "HTTP/1.1", + cookies: [], + headers: request.headers, + queryString: [], + headersSize: -1, + bodySize: -1 + }, + response: { + status: error ? null : endReply.status, + statusText: error ? error.errorString : endReply.statusText, + httpVersion: "HTTP/1.1", + cookies: [], + headers: endReply.headers, + redirectURL: "", + headersSize: -1, + bodySize: startReply.bodySize, + content: { + size: startReply.bodySize, + mimeType: endReply.contentType + } + }, + cache: {}, + timings: { + blocked: 0, + dns: -1, + connect: -1, + send: 0, + wait: startReply.time - request.time, + receive: endReply.time - startReply.time, + ssl: -1 + }, + pageref: page.url + }); + } else if (error) { + entries.push({ + startedDateTime: request.time.toISOString(), + time: 0, + request: { + method: request.method, + url: request.url, + httpVersion: "HTTP/1.1", + cookies: [], + headers: request.headers, + queryString: [], + headersSize: -1, + bodySize: -1 + }, + response: { + status: null, + statusText: error.errorString, + httpVersion: "HTTP/1.1", + cookies: [], + headers: [], + redirectURL: "", + headersSize: -1, + bodySize: 0, + content: {} + }, + cache: {}, + timings: { + blocked: 0, + dns: -1, + connect: -1, + send: 0, + wait: 0, + receive: 0, + ssl: -1 + }, + pageref: page.url + }); + } + }); + + return { + log: { + version: '1.2', + creator: { + name: "PhantomJS", + version: phantom.version.major + '.' + phantom.version.minor + + '.' + phantom.version.patch + }, + pages: [{ + startedDateTime: (page.startTime instanceof Date) + ? page.startTime.toISOString() : null, + id: page.url, + title: page.title, + pageTimings: { + onLoad: (page.startTime instanceof Date && page.endTime instanceof Date) + ? page.endTime.getTime() - page.startTime.getTime() : null + } + }], + entries: entries + } + }; +}; diff --git a/src/ghostdriver/third_party/parseuri.js b/src/ghostdriver/third_party/parseuri.js new file mode 100644 index 0000000..fba2d20 --- /dev/null +++ b/src/ghostdriver/third_party/parseuri.js @@ -0,0 +1,70 @@ +/* +This file is part of the GhostDriver by Ivan De Marino <http://ivandemarino.me>. + +Copyright (c) 2012-2014, Ivan De Marino <http://ivandemarino.me> +Copyright (c) 2014, Steven Levithan <stevenlevithan.com> +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// parseUri 1.3 +// This code was modified to fit the purpose of GhostDriver +// URL: http://blog.stevenlevithan.com/archives/parseuri + +function parseUri (str) { + var o = parseUri.options, + m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), + uri = {}, + i = 14; + + while (i--) uri[o.key[i]] = m[i] || ""; + + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) uri[o.q.name][$1] = $2; + }); + + uri["chunks"] = (uri["source"].charAt(0) === '/') + ? uri["source"].substr(1).split('/') + : uri["source"].split('/'); + + return uri; +}; + +parseUri.options = { + strictMode: false, + key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: { + strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + } +}; + +// Adapt this to CommonJS Module Require +if (exports) { + exports.options = parseUri.options; + exports.parse = parseUri; +} diff --git a/src/ghostdriver/third_party/uuid.js b/src/ghostdriver/third_party/uuid.js new file mode 100644 index 0000000..b7820ae --- /dev/null +++ b/src/ghostdriver/third_party/uuid.js @@ -0,0 +1,249 @@ +// node-uuid/uuid.js +// +// Copyright (c) 2010 Robert Kieffer +// Dual licensed under the MIT and GPL licenses. +// Documentation and details at https://github.com/broofa/node-uuid +(function() { + var _global = this; + + // Unique ID creation requires a high quality random # generator, but + // Math.random() does not guarantee "cryptographic quality". So we feature + // detect for more robust APIs, normalizing each method to return 128-bits + // (16 bytes) of random data. + var mathRNG, nodeRNG, whatwgRNG; + + // Math.random()-based RNG. All platforms, very fast, unknown quality + var _rndBytes = new Array(16); + mathRNG = function() { + var r, b = _rndBytes, i = 0; + + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) == 0) r = Math.random() * 0x100000000; + b[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return b; + } + + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // WebKit only (currently), moderately fast, high quality + if (_global.crypto && crypto.getRandomValues) { + var _rnds = new Uint32Array(4); + whatwgRNG = function() { + crypto.getRandomValues(_rnds); + + for (var c = 0 ; c < 16; c++) { + _rndBytes[c] = _rnds[c >> 2] >>> ((c & 0x03) * 8) & 0xff; + } + return _rndBytes; + } + } + + // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html + // Node.js only, moderately fast, high quality + try { + var _rb = require('crypto').randomBytes; + nodeRNG = _rb && function() { + return _rb(16); + }; + } catch (e) {} + + // Select RNG with best quality + var _rng = nodeRNG || whatwgRNG || mathRNG; + + // Buffer class to use + var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array; + + // Maps for number <-> hex string conversion + var _byteToHex = []; + var _hexToByte = {}; + for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; + } + + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; + } + + // **`unparse()` - Convert UUID byte array (ala parse()) into a string** + function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; + } + + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html + + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); + + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] + ]; + + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + + // Previous uuid creation time + var _lastMSecs = 0, _lastNSecs = 0; + + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq != null ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs != null ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq == null) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); + } + + // **`v4()` - Generate random UUID** + + // See https://github.com/broofa/node-uuid for API details + function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new BufferClass(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); + } + + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; + uuid.BufferClass = BufferClass; + + // Export RNG options + uuid.mathRNG = mathRNG; + uuid.nodeRNG = nodeRNG; + uuid.whatwgRNG = whatwgRNG; + + if (typeof(module) != 'undefined') { + // Play nice with node.js + module.exports = uuid; + } else { + // Play nice with browsers + var _previousRoot = _global.uuid; + + // **`noConflict()` - (browser only) to reset global 'uuid' var** + uuid.noConflict = function() { + _global.uuid = _previousRoot; + return uuid; + } + _global.uuid = uuid; + } +}()); diff --git a/src/ghostdriver/webdriver_atoms.js b/src/ghostdriver/webdriver_atoms.js new file mode 100644 index 0000000..4f1f747 --- /dev/null +++ b/src/ghostdriver/webdriver_atoms.js @@ -0,0 +1,44 @@ +/* +This file is part of the GhostDriver by Ivan De Marino <http://ivandemarino.me>. + +Copyright (c) 2012-2014, Ivan De Marino <http://ivandemarino.me> +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var fs = require("fs"), + atomsCache = {}; + +exports.get = function(atomName) { + var atomFileName = module.dirname + "/third_party/webdriver-atoms/" + atomName + ".js"; + + // Check if we have already loaded an cached this Atom + if (!atomsCache.hasOwnProperty(atomName)) { + try { + atomsCache[atomName] = fs.read(atomFileName); + } catch (e) { + throw new Error("Unable to load Atom '"+atomName+"' from file '"+atomFileName+"'"); + } + } + + return atomsCache[atomName]; +}; diff --git a/src/ghostdriver/webelementlocator.js b/src/ghostdriver/webelementlocator.js new file mode 100644 index 0000000..e9e7cfb --- /dev/null +++ b/src/ghostdriver/webelementlocator.js @@ -0,0 +1,258 @@ +/* +This file is part of the GhostDriver by Ivan De Marino <http://ivandemarino.me>. + +Copyright (c) 2012-2014, Ivan De Marino <http://ivandemarino.me> +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var ghostdriver = ghostdriver || {}; + +ghostdriver.WebElementLocator = function(session) { + // private: + const + _supportedStrategies = [ + "class name", "className", //< Returns an element whose class name contains the search value; compound class names are not permitted. + "css", "css selector", //< Returns an element matching a CSS selector. + "id", //< Returns an element whose ID attribute matches the search value. + "name", //< Returns an element whose NAME attribute matches the search value. + "link text", "linkText", //< Returns an anchor element whose visible text matches the search value. + "partial link text", "partialLinkText", //< Returns an anchor element whose visible text partially matches the search value. + "tag name", "tagName", //< Returns an element whose tag name matches the search value. + "xpath" //< Returns an element matching an XPath expression. + ]; + + var + _session = session, + _errors = require("./errors.js"), + _log = ghostdriver.logger.create("WebElementLocator"), + + _find = function(what, locator, rootElement) { + var currWindow = _session.getCurrentWindow(), + findRes, + findAtom = require("./webdriver_atoms.js").get( + "find_" + + (what.indexOf("element") >= 0 ? what : "element")), //< normalize + errorMsg; + + if (currWindow !== null && + locator && typeof(locator) === "object" && + locator.using && locator.value && //< if well-formed input + _supportedStrategies.indexOf(locator.using) >= 0) { //< and if strategy is recognized + + _log.debug("_find.locator", JSON.stringify(locator)); + + // Ensure "rootElement" is valid, otherwise undefine-it + if (!rootElement || typeof(rootElement) !== "object" || !rootElement["ELEMENT"]) { + rootElement = undefined; + } + + // Use Atom "find_result" to search for element in the page + findRes = currWindow.evaluate( + findAtom, + locator.using, + locator.value, + rootElement); + + // De-serialise the result of the Atom execution + try { + return JSON.parse(findRes); + } catch (e) { + errorMsg = JSON.stringify(locator); + _log.error("_find.locator.error", errorMsg); + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.UnknownCommand, + "value" : errorMsg + }; + } + } + + // Window was not found + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, + "value" : "No such window" + }; + }, + + _locateElement = function(locator, rootElement) { + var findElementRes = _find("element", locator, rootElement); + + _log.debug("_locateElement.locator", JSON.stringify(locator)); + _log.debug("_locateElement.findElementResult", JSON.stringify(findElementRes)); + + // To check if element was found, the following must happen: + // 1. "findElementRes" result object must be valid + // 2. property "status" is found and is {Number} + if (findElementRes !== null && typeof(findElementRes) === "object" && + findElementRes.hasOwnProperty("status") && typeof(findElementRes.status) === "number") { + // If the atom succeeds, but returns a null value, the element was not found. + if (findElementRes.status === 0 && findElementRes.value === null) { + findElementRes.status = _errors.FAILED_CMD_STATUS_CODES.NoSuchElement; + findElementRes.value = { + "message": "Unable to find element with " + + locator.using + " '" + + locator.value + "'" + }; + } + return findElementRes; + } + + // Not found + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.NoSuchElement, + "value" : "No Such Element found" + }; + }, + + _locateElements = function(locator, rootElement) { + var findElementsRes = _find("elements", locator, rootElement), + elements = []; + + _log.debug("_locateElements.locator", JSON.stringify(locator)); + _log.debug("_locateElements.findElementsResult", JSON.stringify(findElementsRes)); + + // To check if something was found, the following must happen: + // 1. "findElementsRes" result object must be valid + // 2. property "status" is found and is {Number} + // 3. property "value" is found and is and {Object} + if (findElementsRes !== null && typeof(findElementsRes) === "object" && + findElementsRes.hasOwnProperty("status") && typeof(findElementsRes.status) === "number" && + findElementsRes.hasOwnProperty("value") && findElementsRes.value !== null && typeof(findElementsRes.value) === "object") { + return findElementsRes; + } + + // Not found + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.NoSuchElement, + "value" : "No Such Elements found" + }; + }, + + _locateActiveElement = function() { + var currWindow = _session.getCurrentWindow(), + activeElementRes; + + if (currWindow !== null) { + activeElementRes = currWindow.evaluate( + require("./webdriver_atoms.js").get("active_element")); + + // De-serialise the result of the Atom execution + try { + activeElementRes = JSON.parse(activeElementRes); + } catch (e) { + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.NoSuchElement, + "value" : "No Active Element found" + }; + } + + // If found + if (typeof(activeElementRes.status) !== "undefined") { + return activeElementRes; + } + } + + return { + "status" : _errors.FAILED_CMD_STATUS_CODES.NoSuchWindow, + "value" : "No such window" + }; + }, + + _handleLocateCommand = function(req, res, locatorMethod, rootElement, startTime) { + // Search for a WebElement on the Page + var elementOrElements, + searchStartTime = startTime || new Date().getTime(), + stopSearchByTime, + request = {}; + + _log.debug("_handleLocateCommand", "Element(s) Search Start Time: " + searchStartTime); + + // If a "locatorMethod" was not provided, default to "locateElement" + if(typeof(locatorMethod) !== "function") { + locatorMethod = _locateElement; + } + + // Some language bindings can send a null instead of an empty + // JSON object for the getActiveElement command. + if (req.post && typeof req.post === "string") { + request = JSON.parse(req.post); + } + + // Try to find the element + elementOrElements = locatorMethod(request, rootElement); + + _log.debug("_handleLocateCommand.elements", JSON.stringify(elementOrElements)); + _log.debug("_handleLocateCommand.rootElement", (typeof(rootElement) !== "undefined" ? JSON.stringify(rootElement) : "BODY")); + + if (elementOrElements && + elementOrElements.hasOwnProperty("status") && + elementOrElements.status === 0 && + elementOrElements.hasOwnProperty("value")) { + + // return if elements found OR we passed the "stopSearchByTime" + stopSearchByTime = searchStartTime + _session.getImplicitTimeout(); + if (elementOrElements.value.length !== 0 || new Date().getTime() > stopSearchByTime) { + + _log.debug("_handleLocateCommand", "Element(s) Found. Search Stop Time: " + stopSearchByTime); + + res.success(_session.getId(), elementOrElements.value); + return; + } + } + + // retry if we haven't passed "stopSearchByTime" + stopSearchByTime = searchStartTime + _session.getImplicitTimeout(); + if (stopSearchByTime >= new Date().getTime()) { + + _log.debug("_handleLocateCommand", "Element(s) NOT Found: RETRY. Search Stop Time: " + stopSearchByTime); + + // Recursive call in 50ms + setTimeout(function(){ + _handleLocateCommand(req, res, locatorMethod, rootElement, searchStartTime); + }, 50); + return; + } + + // Error handler. We got a valid response, but it was an error response. + if (elementOrElements) { + + _log.error("_handleLocateCommand", "Element(s) NOT Found: GAVE UP. Search Stop Time: " + stopSearchByTime); + + _errors.handleFailedCommandEH(elementOrElements.status, + elementOrElements.value.message, + req, + res, + _session); + return; + } + + throw _errors.createInvalidReqVariableResourceNotFoundEH(req); + }; + + // public: + return { + locateElement : _locateElement, + locateElements : _locateElements, + locateActiveElement : _locateActiveElement, + handleLocateCommand : _handleLocateCommand + }; +}; diff --git a/src/linenoise/README.md b/src/linenoise/README.md new file mode 100644 index 0000000..652ac9b --- /dev/null +++ b/src/linenoise/README.md @@ -0,0 +1,18 @@ +This project contains the **Linenoise project**, initially released +by [Salvatore Sanfilippo](https://github.com/antirez). Here we import a fork +by [Tad Marshall](https://github.com/tadmarshall) that lives at +[github.com/tadmarshall/linenoise](https://github.com/tadmarshall/linenoise). + +The version of Linenoise included in PhantomJS refers to the commit: +----- +commit 7946e2c2d08df11dca2b99c5db40360c3d3e9a80 +Author: Alan T. DeKok <aland@freeradius.org> +Date: Wed Oct 26 15:56:52 2011 +0200 + + Added character callbacks again +----- + +Some files not needed for PhantomJS are removed. + +Linenoise is licensed under the BSD-license. +Kudos to all the developers that contribute to this nice little pearl. diff --git a/src/linenoise/linenoise.pri b/src/linenoise/linenoise.pri new file mode 100644 index 0000000..9095bae --- /dev/null +++ b/src/linenoise/linenoise.pri @@ -0,0 +1,9 @@ +VPATH += $$PWD/src +INCLUDEPATH += $$PWD/src + +DEFINES += USE_UTF8 + +SOURCES += linenoise.c \ + utf8.c +HEADERS += linenoise.h \ + utf8.h diff --git a/src/linenoise/src/README.markdown b/src/linenoise/src/README.markdown new file mode 100644 index 0000000..6fe1ee8 --- /dev/null +++ b/src/linenoise/src/README.markdown @@ -0,0 +1,51 @@ +# Linenoise + +A minimal, zero-config, BSD licensed, readline replacement. + +News: linenoise now includes minimal completion support, thanks to Pieter Noordhuis (@pnoordhuis). + +News: linenoise is now part of [Android](http://android.git.kernel.org/?p=platform/system/core.git;a=tree;f=liblinenoise;h=56450eaed7f783760e5e6a5993ef75cde2e29dea;hb=HEAD Android)! + +News: Win32 port, many cleanups and fixes from https://github.com/msteveb/linenoise. + +News: added minimal character callbacks, so that the application can catch characters entered by the user, from Alan DeKok. + +## Can a line editing library be 20k lines of code? + +Line editing with some support for history is a really important feature for command line utilities. Instead of retyping almost the same stuff again and again it's just much better to hit the up arrow and edit on syntax errors, or in order to try a slightly different command. But apparently code dealing with terminals is some sort of Black Magic: readline is 30k lines of code, libedit 20k. Is it reasonable to link small utilities to huge libraries just to get a minimal support for line editing? + +So what usually happens is either: + + * Large programs with configure scripts disabling line editing if readline is not present in the system, or not supporting it at all since readline is GPL licensed and libedit (the BSD clone) is not as known and available as readline is (Real world example of this problem: Tclsh). + * Smaller programs not using a configure script not supporting line editing at all (A problem we had with Redis-cli for instance). + +The result is a pollution of binaries without line editing support. + +So I spent more or less two hours doing a reality check resulting in this little library: is it *really* needed for a line editing library to be 20k lines of code? Apparently not, it is possibe to get a very small, zero configuration, trivial to embed library, that solves the problem. Smaller programs will just include this, supporting line editing out of the box. Larger programs may use this little library or just checking with configure if readline/libedit is available and resorting to linenoise if not. + +## Terminals, in 2010. + +Apparently almost every terminal you can happen to use today has some kind of support for VT100 alike escape sequences. So I tried to write a lib using just very basic VT100 features. The resulting library appears to work everywhere I tried to use it. + +Since it's so young I guess there are a few bugs, or the lib may not compile or work with some operating system, but it's a matter of a few weeks and eventually we'll get it right, and there will be no excuses for not shipping command line tools without built-in line editing support. + +The library started off at less than 400 lines of code, and is now almost 1500 lines of code. However, it now includes support for UTF-8 and Win32. In order to use it in your project just look at the *example.c* file in the source distribution, it is trivial. Linenoise is BSD code, so you can use both in free software and commercial software. + +## Tested with... + + * Linux text only console ($TERM = linux) + * Linux KDE terminal application ($TERM = xterm) + * Linux xterm ($TERM = xterm) + * Mac OS X iTerm ($TERM = xterm) + * Mac OS X default Terminal.app ($TERM = xterm) + * OpenBSD 4.5 through an OSX Terminal.app ($TERM = screen) + * IBM AIX 6.1 + * FreeBSD xterm ($TERM = xterm) + +Please test it everywhere you can and report back! + +## Let's push this forward! + +Please fork it and add something interesting and send me a pull request. What's especially interesting are fixes, new key bindings, completion. + +Send feedbacks to antirez at gmail diff --git a/src/linenoise/src/example.c b/src/linenoise/src/example.c new file mode 100644 index 0000000..e74125f --- /dev/null +++ b/src/linenoise/src/example.c @@ -0,0 +1,90 @@ +#include <stdio.h> +#include <stdlib.h> +#include "linenoise.h" + +#ifndef NO_COMPLETION +void completion(const char *buf, linenoiseCompletions *lc) { + if (buf[0] == 'h') { + linenoiseAddCompletion(lc,"hello"); + linenoiseAddCompletion(lc,"hello there"); + } +} +#endif + +static int in_string = 0; +static size_t string_start = 0; + +int foundspace(const char *buf, size_t len, char c) { + if (in_string) return 0; + + if (len == 0) return 1; + + if (buf[len -1] == c) return 1; + + printf("\r\nSPACE!\r\n"); + return 0; +} + +int escapedquote(const char *start) +{ + while (*start) { + if (*start == '\\') { + if (!start[1]) return 1; + start += 2; + } + start++; + } + return 0; +} + + +int foundquote(const char *buf, size_t len, char c) { + if (!in_string) { + in_string = 1; + string_start = len; + return 0; + } + + if (buf[string_start] != c) return 0; + + if (escapedquote(buf + string_start)) return 0; + + in_string = 0; + printf("\r\nSTRING %s%c\r\n", buf + string_start, buf[string_start]); + string_start = 0; + + return 0; +} + +int foundhelp(const char *buf, size_t len, char c) { + if (in_string) return 0; + + len = len; /* -Wunused */ + c = c; /* -Wunused */ + + printf("?\r\nHELP: %s\r\n", buf); + return 1; +} + +int main(void) { + char *line; + +#ifndef NO_COMPLETION + linenoiseSetCompletionCallback(completion); +#endif + linenoiseHistoryLoad("history.txt"); /* Load the history at startup */ + linenoiseSetCharacterCallback(foundspace, ' '); + linenoiseSetCharacterCallback(foundquote, '"'); + linenoiseSetCharacterCallback(foundquote, '\''); + linenoiseSetCharacterCallback(foundhelp, '?'); + + while((line = linenoise("hello> ")) != NULL) { + if (line[0] != '\0') { + printf("echo: '%s'\n", line); + linenoiseHistoryAdd(line); + linenoiseHistorySave("history.txt"); /* Save every new entry */ + } + free(line); + } + return 0; +} diff --git a/src/linenoise/src/linenoise.c b/src/linenoise/src/linenoise.c new file mode 100644 index 0000000..b00a308 --- /dev/null +++ b/src/linenoise/src/linenoise.c @@ -0,0 +1,1411 @@ +/* linenoise.c -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * You can find the latest source code at: + * + * http://github.com/alandekok/linenoise + * (forked from http://github.com/antirez/linenoise) + * (merged with a number of other linenoise forks) + * + * Does a number of crazy assumptions that happen to be true in 99.9999% of + * the 2010 UNIX computers around. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com> + * Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com> + * Copyright (c) 2011, Steve Bennett <steveb at workware dot net dot au> + * Copyright (c) 2011, Alan DeKok <steveb at freeradius dot org> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------------------------ + * + * References: + * - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + * + * Bloat: + * - Completion? + * + * Unix/termios + * ------------ + * List of escape sequences used by this program, we do everything just + * a few sequences. In order to be so cheap we may have some + * flickering effect with some slow terminal, but the lesser sequences + * the more compatible. + * + * CHA (Cursor Horizontal Absolute) + * Sequence: ESC [ n G + * Effect: moves cursor to column n (1 based) + * + * EL (Erase Line) + * Sequence: ESC [ n K + * Effect: if n is 0 or missing, clear from cursor to end of line + * Effect: if n is 1, clear from beginning of line to cursor + * Effect: if n is 2, clear entire line + * + * CUF (CUrsor Forward) + * Sequence: ESC [ n C + * Effect: moves cursor forward of n chars + * + * The following are used to clear the screen: ESC [ H ESC [ 2 J + * This is actually composed of two sequences: + * + * cursorhome + * Sequence: ESC [ H + * Effect: moves the cursor to upper left corner + * + * ED2 (Clear entire screen) + * Sequence: ESC [ 2 J + * Effect: clear the whole screen + * + * == For highlighting control characters, we also use the following two == + * SO (enter StandOut) + * Sequence: ESC [ 7 m + * Effect: Uses some standout mode such as reverse video + * + * SE (Standout End) + * Sequence: ESC [ 0 m + * Effect: Exit standout mode + * + * == Only used if TIOCGWINSZ fails == + * DSR/CPR (Report cursor position) + * Sequence: ESC [ 6 n + * Effect: reports current cursor position as ESC [ NNN ; MMM R + * + * win32/console + * ------------- + * If __MINGW32__ is defined, the win32 console API is used. + * This could probably be made to work for the msvc compiler too. + * This support based in part on work by Jon Griffiths. + */ + +#ifdef _WIN32 /* Windows platform, either MinGW or Visual Studio (MSVC) */ +#include <windows.h> +#include <fcntl.h> +#define USE_WINCONSOLE +#ifdef __MINGW32__ +#define HAVE_UNISTD_H +#else +/* Microsoft headers don't like old POSIX names */ +#define strdup _strdup +#define snprintf _snprintf +#endif +#else +#include <termios.h> +#include <sys/ioctl.h> +#include <sys/poll.h> +#define USE_TERMIOS +#define HAVE_UNISTD_H +#endif + +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <stdlib.h> +#include <stdarg.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <sys/types.h> + +#include "linenoise.h" +#include "utf8.h" + +#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100 +#define LINENOISE_MAX_LINE 4096 +static linenoiseCharacterCallback *characterCallback[256] = { NULL }; + +#define ctrl(C) ((C) - '@') + +/* Use -ve numbers here to co-exist with normal unicode chars */ +enum { + SPECIAL_NONE, + SPECIAL_UP = -20, + SPECIAL_DOWN = -21, + SPECIAL_LEFT = -22, + SPECIAL_RIGHT = -23, + SPECIAL_DELETE = -24, + SPECIAL_HOME = -25, + SPECIAL_END = -26, +}; + +static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; +static int history_len = 0; +static char **history = NULL; + +/* Structure to contain the status of the current (being edited) line */ +struct current { + char *buf; /* Current buffer. Always null terminated */ + int bufmax; /* Size of the buffer, including space for the null termination */ + int len; /* Number of bytes in 'buf' */ + int chars; /* Number of chars in 'buf' (utf-8 chars) */ + int pos; /* Cursor position, measured in chars */ + int cols; /* Size of the window, in chars */ + const char *prompt; +#if defined(USE_TERMIOS) + int fd; /* Terminal fd */ +#elif defined(USE_WINCONSOLE) + HANDLE outh; /* Console output handle */ + HANDLE inh; /* Console input handle */ + int rows; /* Screen rows */ + int x; /* Current column during output */ + int y; /* Current row */ +#endif +}; + +static int fd_read(struct current *current); +static int getWindowSize(struct current *current); + +void linenoiseHistoryFree(void) { + if (history) { + int j; + + for (j = 0; j < history_len; j++) + free(history[j]); + free(history); + history = NULL; + } +} + +#if defined(USE_TERMIOS) +static void linenoiseAtExit(void); +static struct termios orig_termios; /* in order to restore at exit */ +static int rawmode = 0; /* for atexit() function to check if restore is needed*/ +static int atexit_registered = 0; /* register atexit just 1 time */ + +static const char *unsupported_term[] = {"dumb","cons25",NULL}; + +static int isUnsupportedTerm(void) { + char *term = getenv("TERM"); + + if (term) { + int j; + for (j = 0; unsupported_term[j]; j++) { + if (strcasecmp(term, unsupported_term[j]) == 0) { + return 1; + } + } + } + return 0; +} + +static int enableRawMode(struct current *current) { + struct termios raw; + + current->fd = STDIN_FILENO; + + if (!isatty(current->fd) || isUnsupportedTerm() || + tcgetattr(current->fd, &orig_termios) == -1) { +fatal: + errno = ENOTTY; + return -1; + } + + if (!atexit_registered) { + atexit(linenoiseAtExit); + atexit_registered = 1; + } + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(current->fd,TCSADRAIN,&raw) < 0) { + goto fatal; + } + rawmode = 1; + + current->cols = 0; + return 0; +} + +static void disableRawMode(struct current *current) { + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(current->fd,TCSADRAIN,&orig_termios) != -1) + rawmode = 0; +} + +/* At exit we'll try to fix the terminal to the initial conditions. */ +static void linenoiseAtExit(void) { + if (rawmode) { + tcsetattr(STDIN_FILENO, TCSADRAIN, &orig_termios); + } + linenoiseHistoryFree(); +} + +/* gcc/glibc insists that we care about the return code of write! */ +#define IGNORE_RC(EXPR) if (EXPR) {} + +/* This is fdprintf() on some systems, but use a different + * name to avoid conflicts + */ +static void fd_printf(int fd, const char *format, ...) +{ + va_list args; + char buf[64]; + int n; + + va_start(args, format); + n = vsnprintf(buf, sizeof(buf), format, args); + va_end(args); + IGNORE_RC(write(fd, buf, n)); +} + +static void clearScreen(struct current *current) +{ + fd_printf(current->fd, "\x1b[H\x1b[2J"); +} + +static void cursorToLeft(struct current *current) +{ + fd_printf(current->fd, "\x1b[1G"); +} + +static int outputChars(struct current *current, const char *buf, int len) +{ + return write(current->fd, buf, len); +} + +static void outputControlChar(struct current *current, char ch) +{ + fd_printf(current->fd, "\033[7m^%c\033[0m", ch); +} + +static void eraseEol(struct current *current) +{ + fd_printf(current->fd, "\x1b[0K"); +} + +static void setCursorPos(struct current *current, int x) +{ + fd_printf(current->fd, "\x1b[1G\x1b[%dC", x); +} + +/** + * Reads a char from 'fd', waiting at most 'timeout' milliseconds. + * + * A timeout of -1 means to wait forever. + * + * Returns -1 if no char is received within the time or an error occurs. + */ +static int fd_read_char(int fd, int timeout) +{ + struct pollfd p; + unsigned char c; + + p.fd = fd; + p.events = POLLIN; + + if (poll(&p, 1, timeout) == 0) { + /* timeout */ + return -1; + } + if (read(fd, &c, 1) != 1) { + return -1; + } + return c; +} + +/** + * Reads a complete utf-8 character + * and returns the unicode value, or -1 on error. + */ +static int fd_read(struct current *current) +{ +#ifdef USE_UTF8 + char buf[4]; + int n; + int i; + int c; + + if (read(current->fd, &buf[0], 1) != 1) { + return -1; + } + n = utf8_charlen(buf[0]); + if (n < 1 || n > 3) { + return -1; + } + for (i = 1; i < n; i++) { + if (read(current->fd, &buf[i], 1) != 1) { + return -1; + } + } + buf[n] = 0; + /* decode and return the character */ + utf8_tounicode(buf, &c); + return c; +#else + return fd_read_char(current->fd, -1); +#endif +} + +static int getWindowSize(struct current *current) +{ + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col != 0) { + current->cols = ws.ws_col; + return 0; + } + + /* Failed to query the window size. Perhaps we are on a serial terminal. + * Try to query the width by sending the cursor as far to the right + * and reading back the cursor position. + * Note that this is only done once per call to linenoise rather than + * every time the line is refreshed for efficiency reasons. + */ + if (current->cols == 0) { + current->cols = 80; + + /* Move cursor far right and report cursor position */ + fd_printf(current->fd, "\x1b[999G" "\x1b[6n"); + + /* Parse the response: ESC [ rows ; cols R */ + if (fd_read_char(current->fd, 100) == 0x1b && fd_read_char(current->fd, 100) == '[') { + int n = 0; + while (1) { + int ch = fd_read_char(current->fd, 100); + if (ch == ';') { + /* Ignore rows */ + n = 0; + } + else if (ch == 'R') { + /* Got cols */ + if (n != 0 && n < 1000) { + current->cols = n; + } + break; + } + else if (ch >= 0 && ch <= '9') { + n = n * 10 + ch - '0'; + } + else { + break; + } + } + } + } + return 0; +} + +/** + * If escape (27) was received, reads subsequent + * chars to determine if this is a known special key. + * + * Returns SPECIAL_NONE if unrecognised, or -1 if EOF. + * + * If no additional char is received within a short time, + * 27 is returned. + */ +static int check_special(int fd) +{ + int c = fd_read_char(fd, 50); + int c2; + + if (c < 0) { + return 27; + } + + c2 = fd_read_char(fd, 50); + if (c2 < 0) { + return c2; + } + if (c == '[' || c == 'O') { + /* Potential arrow key */ + switch (c2) { + case 'A': + return SPECIAL_UP; + case 'B': + return SPECIAL_DOWN; + case 'C': + return SPECIAL_RIGHT; + case 'D': + return SPECIAL_LEFT; + case 'F': + return SPECIAL_END; + case 'H': + return SPECIAL_HOME; + } + } + if (c == '[' && c2 >= '1' && c2 <= '8') { + /* extended escape */ + c = fd_read_char(fd, 50); + if (c == '~') { + switch (c2) { + case '3': + return SPECIAL_DELETE; + case '7': + return SPECIAL_HOME; + case '8': + return SPECIAL_END; + } + } + while (c != -1 && c != '~') { + /* .e.g \e[12~ or '\e[11;2~ discard the complete sequence */ + c = fd_read_char(fd, 50); + } + } + + return SPECIAL_NONE; +} +#elif defined(USE_WINCONSOLE) + +static DWORD orig_consolemode = 0; + +static int enableRawMode(struct current *current) { + DWORD n; + INPUT_RECORD irec; + + current->outh = GetStdHandle(STD_OUTPUT_HANDLE); + current->inh = GetStdHandle(STD_INPUT_HANDLE); + + if (!PeekConsoleInput(current->inh, &irec, 1, &n)) { + return -1; + } + if (getWindowSize(current) != 0) { + return -1; + } + if (GetConsoleMode(current->inh, &orig_consolemode)) { + SetConsoleMode(current->inh, ENABLE_PROCESSED_INPUT); + } + return 0; +} + +static void disableRawMode(struct current *current) +{ + SetConsoleMode(current->inh, orig_consolemode); +} + +static void clearScreen(struct current *current) +{ + COORD topleft = { 0, 0 }; + DWORD n; + + FillConsoleOutputCharacter(current->outh, ' ', + current->cols * current->rows, topleft, &n); + FillConsoleOutputAttribute(current->outh, + FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN, + current->cols * current->rows, topleft, &n); + SetConsoleCursorPosition(current->outh, topleft); +} + +static void cursorToLeft(struct current *current) +{ + COORD pos = { 0, current->y }; + DWORD n; + + FillConsoleOutputAttribute(current->outh, + FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN, current->cols, pos, &n); + current->x = 0; +} + +static int outputChars(struct current *current, const char *buf, int len) +{ + COORD pos = { current->x, current->y }; + DWORD n; + + WriteConsoleOutputCharacterA(current->outh, buf, len, pos, &n); + current->x += len; + return 0; +} + +static void outputControlChar(struct current *current, char ch) +{ + COORD pos = { current->x, current->y }; + DWORD n; + + FillConsoleOutputAttribute(current->outh, BACKGROUND_INTENSITY, 2, pos, &n); + outputChars(current, "^", 1); + outputChars(current, &ch, 1); +} + +static void eraseEol(struct current *current) +{ + COORD pos = { current->x, current->y }; + DWORD n; + + FillConsoleOutputCharacter(current->outh, ' ', current->cols - current->x, pos, &n); +} + +static void setCursorPos(struct current *current, int x) +{ + COORD pos = { x, current->y }; + + SetConsoleCursorPosition(current->outh, pos); + current->x = x; +} + +static int fd_read(struct current *current) +{ + while (1) { + INPUT_RECORD irec; + DWORD n; + if (WaitForSingleObject(current->inh, INFINITE) != WAIT_OBJECT_0) { + break; + } + if (!ReadConsoleInput (current->inh, &irec, 1, &n)) { + break; + } + if (irec.EventType == KEY_EVENT && irec.Event.KeyEvent.bKeyDown) { + KEY_EVENT_RECORD *k = &irec.Event.KeyEvent; + if (k->dwControlKeyState & ENHANCED_KEY) { + switch (k->wVirtualKeyCode) { + case VK_LEFT: + return SPECIAL_LEFT; + case VK_RIGHT: + return SPECIAL_RIGHT; + case VK_UP: + return SPECIAL_UP; + case VK_DOWN: + return SPECIAL_DOWN; + case VK_DELETE: + return SPECIAL_DELETE; + case VK_HOME: + return SPECIAL_HOME; + case VK_END: + return SPECIAL_END; + } + } + /* Note that control characters are already translated in AsciiChar */ + else { +#ifdef USE_UTF8 + return k->uChar.UnicodeChar; +#else + return k->uChar.AsciiChar; +#endif + } + } + } + return -1; +} + +static int getWindowSize(struct current *current) +{ + CONSOLE_SCREEN_BUFFER_INFO info; + if (!GetConsoleScreenBufferInfo(current->outh, &info)) { + return -1; + } + current->cols = info.dwSize.X; + current->rows = info.dwSize.Y; + if (current->cols <= 0 || current->rows <= 0) { + current->cols = 80; + return -1; + } + current->y = info.dwCursorPosition.Y; + current->x = info.dwCursorPosition.X; + return 0; +} +#endif + +static int utf8_getchars(char *buf, int c) +{ +#ifdef USE_UTF8 + return utf8_fromunicode(buf, c); +#else + *buf = c; + return 1; +#endif +} + +/** + * Returns the unicode character at the given offset, + * or -1 if none. + */ +static int get_char(struct current *current, int pos) +{ + if (pos >= 0 && pos < current->chars) { + int c; + int i = utf8_index(current->buf, pos); + (void)utf8_tounicode(current->buf + i, &c); + return c; + } + return -1; +} + +static void refreshLine(const char *prompt, struct current *current) +{ + int plen; + int pchars; + int backup = 0; + int i; + const char *buf = current->buf; + int chars = current->chars; + int pos = current->pos; + int b; + int ch; + int n; + + /* Should intercept SIGWINCH. For now, just get the size every time */ + getWindowSize(current); + + plen = strlen(prompt); + pchars = utf8_strlen(prompt, plen); + + /* Account for a line which is too long to fit in the window. + * Note that control chars require an extra column + */ + + /* How many cols are required to the left of 'pos'? + * The prompt, plus one extra for each control char + */ + n = pchars + utf8_strlen(buf, current->len); + b = 0; + for (i = 0; i < pos; i++) { + b += utf8_tounicode(buf + b, &ch); + if (ch < ' ') { + n++; + } + } + + /* If too many are need, strip chars off the front of 'buf' + * until it fits. Note that if the current char is a control character, + * we need one extra col. + */ + if (current->pos < current->chars && get_char(current, current->pos) < ' ') { + n++; + } + + while (n >= current->cols) { + b = utf8_tounicode(buf, &ch); + if (ch < ' ') { + n--; + } + n--; + buf += b; + pos--; + chars--; + } + + /* Cursor to left edge, then the prompt */ + cursorToLeft(current); + outputChars(current, prompt, plen); + + /* Now the current buffer content */ + + /* Need special handling for control characters. + * If we hit 'cols', stop. + */ + b = 0; /* unwritted bytes */ + n = 0; /* How many control chars were written */ + for (i = 0; i < chars; i++) { + int ch; + int w = utf8_tounicode(buf + b, &ch); + if (ch < ' ') { + n++; + } + if (pchars + i + n >= current->cols) { + break; + } + if (ch < ' ') { + /* A control character, so write the buffer so far */ + outputChars(current, buf, b); + buf += b + w; + b = 0; + outputControlChar(current, ch + '@'); + if (i < pos) { + backup++; + } + } + else { + b += w; + } + } + outputChars(current, buf, b); + + /* Erase to right, move cursor to original position */ + eraseEol(current); + setCursorPos(current, pos + pchars + backup); +} + +static void set_current(struct current *current, const char *str) +{ + strncpy(current->buf, str, current->bufmax); + current->buf[current->bufmax - 1] = 0; + current->len = strlen(current->buf); + current->pos = current->chars = utf8_strlen(current->buf, current->len); +} + +static int has_room(struct current *current, int bytes) +{ + return current->len + bytes < current->bufmax - 1; +} + +/** + * Removes the char at 'pos'. + * + * Returns 1 if the line needs to be refreshed, 2 if not + * and 0 if nothing was removed + */ +static int remove_char(struct current *current, int pos) +{ + if (pos >= 0 && pos < current->chars) { + int p1, p2; + int ret = 1; + p1 = utf8_index(current->buf, pos); + p2 = p1 + utf8_index(current->buf + p1, 1); + +#ifdef USE_TERMIOS + /* optimise remove char in the case of removing the last char */ + if (current->pos == pos + 1 && current->pos == current->chars) { + if (current->buf[pos] >= ' ' && utf8_strlen(current->prompt, -1) + utf8_strlen(current->buf, current->len) < current->cols - 1) { + ret = 2; + fd_printf(current->fd, "\b \b"); + } + } +#endif + + /* Move the null char too */ + memmove(current->buf + p1, current->buf + p2, current->len - p2 + 1); + current->len -= (p2 - p1); + current->chars--; + + if (current->pos > pos) { + current->pos--; + } + return ret; + } + return 0; +} + +/** + * Insert 'ch' at position 'pos' + * + * Returns 1 if the line needs to be refreshed, 2 if not + * and 0 if nothing was inserted (no room) + */ +static int insert_char(struct current *current, int pos, int ch) +{ + char buf[3]; + int n = utf8_getchars(buf, ch); + + if (has_room(current, n) && pos >= 0 && pos <= current->chars) { + int p1, p2; + int ret = 1; + p1 = utf8_index(current->buf, pos); + p2 = p1 + n; + +#ifdef USE_TERMIOS + /* optimise the case where adding a single char to the end and no scrolling is needed */ + if (current->pos == pos && current->chars == pos) { + if (ch >= ' ' && utf8_strlen(current->prompt, -1) + utf8_strlen(current->buf, current->len) < current->cols - 1) { + IGNORE_RC(write(current->fd, buf, n)); + ret = 2; + } + } +#endif + + memmove(current->buf + p2, current->buf + p1, current->len - p1); + memcpy(current->buf + p1, buf, n); + current->len += n; + + current->chars++; + if (current->pos >= pos) { + current->pos++; + } + return ret; + } + return 0; +} + +/** + * Returns 0 if no chars were removed or non-zero otherwise. + */ +static int remove_chars(struct current *current, int pos, int n) +{ + int removed = 0; + while (n-- && remove_char(current, pos)) { + removed++; + } + return removed; +} + +#ifndef NO_COMPLETION +static linenoiseCompletionCallback *completionCallback = NULL; + +static void beep() { +#ifdef USE_TERMIOS + fprintf(stderr, "\x7"); + fflush(stderr); +#endif +} + +static void freeCompletions(linenoiseCompletions *lc) { + size_t i; + for (i = 0; i < lc->len; i++) + free(lc->cvec[i]); + free(lc->cvec); +} + +static int completeLine(struct current *current) { + linenoiseCompletions lc = { 0, NULL }; + int c = 0; + + completionCallback(current->buf,&lc); + if (lc.len == 0) { + beep(); + } else { + size_t stop = 0, i = 0; + + while(!stop) { + /* Show completion or original buffer */ + if (i < lc.len) { + struct current tmp = *current; + tmp.buf = lc.cvec[i]; + tmp.pos = tmp.len = strlen(tmp.buf); + tmp.chars = utf8_strlen(tmp.buf, tmp.len); + refreshLine(current->prompt, &tmp); + } else { + refreshLine(current->prompt, current); + } + + c = fd_read(current); + if (c == -1) { + break; + } + + switch(c) { + case '\t': /* tab */ + i = (i+1) % (lc.len+1); + if (i == lc.len) beep(); + break; + case 27: /* escape */ + /* Re-show original buffer */ + if (i < lc.len) { + refreshLine(current->prompt, current); + } + stop = 1; + break; + default: + /* Update buffer and return */ + if (i < lc.len) { + set_current(current,lc.cvec[i]); + } + stop = 1; + break; + } + } + } + + freeCompletions(&lc); + return c; /* Return last read character */ +} + +/* Register a callback function to be called for tab-completion. */ +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *fn) { + completionCallback = fn; +} + +void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { + lc->cvec = (char **)realloc(lc->cvec,sizeof(char*)*(lc->len+1)); + lc->cvec[lc->len++] = strdup(str); +} + +#endif + +static int linenoisePrompt(struct current *current) { + int history_index = 0; + + /* The latest history entry is always our current buffer, that + * initially is just an empty string. */ + linenoiseHistoryAdd(""); + + set_current(current, ""); + refreshLine(current->prompt, current); + + while(1) { + int dir = -1; + int c = fd_read(current); + +#ifndef NO_COMPLETION + /* Only autocomplete when the callback is set. It returns < 0 when + * there was an error reading from fd. Otherwise it will return the + * character that should be handled next. */ + if (c == 9 && completionCallback != NULL) { + c = completeLine(current); + /* Return on errors */ + if (c < 0) return current->len; + /* Read next character when 0 */ + if (c == 0) continue; + } +#endif + +process_char: + if (c == -1) return current->len; +#ifdef USE_TERMIOS + if (c == 27) { /* escape sequence */ + c = check_special(current->fd); + } +#endif + switch(c) { + case '\r': /* enter */ + history_len--; + free(history[history_len]); + return current->len; + case ctrl('C'): /* ctrl-c */ + errno = EAGAIN; + return -1; + case 127: /* backspace */ + case ctrl('H'): + if (remove_char(current, current->pos - 1) == 1) { + refreshLine(current->prompt, current); + } + break; + case ctrl('D'): /* ctrl-d */ + if (current->len == 0) { + /* Empty line, so EOF */ + history_len--; + free(history[history_len]); + return -1; + } + /* Otherwise fall through to delete char to right of cursor */ + case SPECIAL_DELETE: + if (remove_char(current, current->pos) == 1) { + refreshLine(current->prompt, current); + } + break; + case ctrl('W'): /* ctrl-w */ + /* eat any spaces on the left */ + { + int pos = current->pos; + while (pos > 0 && get_char(current, pos - 1) == ' ') { + pos--; + } + + /* now eat any non-spaces on the left */ + while (pos > 0 && get_char(current, pos - 1) != ' ') { + pos--; + } + + if (remove_chars(current, pos, current->pos - pos)) { + refreshLine(current->prompt, current); + } + } + break; + case ctrl('R'): /* ctrl-r */ + { + /* Display the reverse-i-search prompt and process chars */ + char rbuf[50]; + char rprompt[80]; + int rchars = 0; + int rlen = 0; + int searchpos = history_len - 1; + + rbuf[0] = 0; + while (1) { + int n = 0; + const char *p = NULL; + int skipsame = 0; + int searchdir = -1; + + snprintf(rprompt, sizeof(rprompt), "(reverse-i-search)'%s': ", rbuf); + refreshLine(rprompt, current); + c = fd_read(current); + if (c == ctrl('H') || c == 127) { + if (rchars) { + int p = utf8_index(rbuf, --rchars); + rbuf[p] = 0; + rlen = strlen(rbuf); + } + continue; + } +#ifdef USE_TERMIOS + if (c == 27) { + c = check_special(current->fd); + } +#endif + if (c == ctrl('P') || c == SPECIAL_UP) { + /* Search for the previous (earlier) match */ + if (searchpos > 0) { + searchpos--; + } + skipsame = 1; + } + else if (c == ctrl('N') || c == SPECIAL_DOWN) { + /* Search for the next (later) match */ + if (searchpos < history_len) { + searchpos++; + } + searchdir = 1; + skipsame = 1; + } + else if (c >= ' ') { + if (rlen >= (int)sizeof(rbuf) + 3) { + continue; + } + + n = utf8_getchars(rbuf + rlen, c); + rlen += n; + rchars++; + rbuf[rlen] = 0; + + /* Adding a new char resets the search location */ + searchpos = history_len - 1; + } + else { + /* Exit from incremental search mode */ + break; + } + + /* Now search through the history for a match */ + for (; searchpos >= 0 && searchpos < history_len; searchpos += searchdir) { + p = strstr(history[searchpos], rbuf); + if (p) { + /* Found a match */ + if (skipsame && strcmp(history[searchpos], current->buf) == 0) { + /* But it is identical, so skip it */ + continue; + } + /* Copy the matching line and set the cursor position */ + set_current(current,history[searchpos]); + current->pos = utf8_strlen(history[searchpos], p - history[searchpos]); + break; + } + } + if (!p && n) { + /* No match, so don't add it */ + rchars--; + rlen -= n; + rbuf[rlen] = 0; + } + } + if (c == ctrl('G') || c == ctrl('C')) { + /* ctrl-g terminates the search with no effect */ + set_current(current, ""); + c = 0; + } + else if (c == ctrl('J')) { + /* ctrl-j terminates the search leaving the buffer in place */ + c = 0; + } + /* Go process the char normally */ + refreshLine(current->prompt, current); + goto process_char; + } + break; + case ctrl('T'): /* ctrl-t */ + if (current->pos > 0 && current->pos < current->chars) { + c = get_char(current, current->pos); + remove_char(current, current->pos); + insert_char(current, current->pos - 1, c); + refreshLine(current->prompt, current); + } + break; + case ctrl('V'): /* ctrl-v */ + if (has_room(current, 3)) { + /* Insert the ^V first */ + if (insert_char(current, current->pos, c)) { + refreshLine(current->prompt, current); + /* Now wait for the next char. Can insert anything except \0 */ + c = fd_read(current); + + /* Remove the ^V first */ + remove_char(current, current->pos - 1); + if (c != -1) { + /* Insert the actual char */ + insert_char(current, current->pos, c); + } + refreshLine(current->prompt, current); + } + } + break; + case ctrl('B'): + case SPECIAL_LEFT: + if (current->pos > 0) { + current->pos--; + refreshLine(current->prompt, current); + } + break; + case ctrl('F'): + case SPECIAL_RIGHT: + if (current->pos < current->chars) { + current->pos++; + refreshLine(current->prompt, current); + } + break; + case ctrl('P'): + case SPECIAL_UP: + dir = 1; + case ctrl('N'): + case SPECIAL_DOWN: + if (history_len > 1) { + /* Update the current history entry before to + * overwrite it with tne next one. */ + free(history[history_len-1-history_index]); + history[history_len-1-history_index] = strdup(current->buf); + /* Show the new entry */ + history_index += dir; + if (history_index < 0) { + history_index = 0; + break; + } else if (history_index >= history_len) { + history_index = history_len-1; + break; + } + set_current(current, history[history_len-1-history_index]); + refreshLine(current->prompt, current); + } + break; + case ctrl('A'): /* Ctrl+a, go to the start of the line */ + case SPECIAL_HOME: + current->pos = 0; + refreshLine(current->prompt, current); + break; + case ctrl('E'): /* ctrl+e, go to the end of the line */ + case SPECIAL_END: + current->pos = current->chars; + refreshLine(current->prompt, current); + break; + case ctrl('U'): /* Ctrl+u, delete to beginning of line. */ + if (remove_chars(current, 0, current->pos)) { + refreshLine(current->prompt, current); + } + break; + case ctrl('K'): /* Ctrl+k, delete from current to end of line. */ + if (remove_chars(current, current->pos, current->chars - current->pos)) { + refreshLine(current->prompt, current); + } + break; + case ctrl('L'): /* Ctrl+L, clear screen */ + clearScreen(current); + /* Force recalc of window size for serial terminals */ + current->cols = 0; + refreshLine(current->prompt, current); + break; + default: + if (characterCallback[(int)c]) { + int rcode; + + rcode = characterCallback[(int)c](current->buf,current->len,c); + refreshLine(current->prompt, current); + if (rcode == 1) { + continue; + } + } + + /* Only tab is allowed without ^V */ + if (c == '\t' || c >= ' ') { + if (insert_char(current, current->pos, c) == 1) { + refreshLine(current->prompt, current); + } + } + break; + } + } + return current->len; +} + +char *linenoise(const char *prompt) +{ + int count; + struct current current; + char buf[LINENOISE_MAX_LINE]; + + if (enableRawMode(¤t) == -1) { + printf("%s", prompt); + fflush(stdout); + if (fgets(buf, sizeof(buf), stdin) == NULL) { + return NULL; + } + count = strlen(buf); + if (count && buf[count-1] == '\n') { + count--; + buf[count] = '\0'; + } + } + else + { + current.buf = buf; + current.bufmax = sizeof(buf); + current.len = 0; + current.chars = 0; + current.pos = 0; + current.prompt = prompt; + + count = linenoisePrompt(¤t); + disableRawMode(¤t); + printf("\n"); + if (count == -1) { + return NULL; + } + } + return strdup(buf); +} + +/* Register a callback function to be called when a character is pressed */ +void linenoiseSetCharacterCallback(linenoiseCharacterCallback *fn, char c) { + if (c < ' ') return; + + characterCallback[(int)c] = fn; +} + +/* Using a circular buffer is smarter, but a bit more complex to handle. */ +int linenoiseHistoryAdd(const char *line) { + char *linecopy; + + if (history_max_len == 0) return 0; + if (history == NULL) { + history = (char **)malloc(sizeof(char*)*history_max_len); + if (history == NULL) return 0; + memset(history,0,(sizeof(char*)*history_max_len)); + } + + /* do not insert duplicate lines into history */ + if (history_len > 0 && strcmp(line, history[history_len - 1]) == 0) { + return 0; + } + + linecopy = strdup(line); + if (!linecopy) return 0; + if (history_len == history_max_len) { + free(history[0]); + memmove(history,history+1,sizeof(char*)*(history_max_len-1)); + history_len--; + } + history[history_len] = linecopy; + history_len++; + return 1; +} + +int linenoiseHistorySetMaxLen(int len) { + char **newHistory; + + if (len < 1) return 0; + if (history) { + int tocopy = history_len; + + newHistory = (char **)malloc(sizeof(char*)*len); + if (newHistory == NULL) return 0; + if (len < tocopy) tocopy = len; + memcpy(newHistory,history+(history_max_len-tocopy), sizeof(char*)*tocopy); + free(history); + history = newHistory; + } + history_max_len = len; + if (history_len > history_max_len) + history_len = history_max_len; + return 1; +} + +/* Save the history in the specified file. On success 0 is returned + * otherwise -1 is returned. */ +int linenoiseHistorySave(const char *filename) { + FILE *fp = fopen(filename,"w"); + int j; + + if (fp == NULL) return -1; + for (j = 0; j < history_len; j++) { + const char *str = history[j]; + /* Need to encode backslash, nl and cr */ + while (*str) { + if (*str == '\\') { + fputs("\\\\", fp); + } + else if (*str == '\n') { + fputs("\\n", fp); + } + else if (*str == '\r') { + fputs("\\r", fp); + } + else { + fputc(*str, fp); + } + str++; + } + fputc('\n', fp); + } + + fclose(fp); + return 0; +} + +/* Load the history from the specified file. If the file does not exist + * zero is returned and no operation is performed. + * + * If the file exists and the operation succeeded 0 is returned, otherwise + * on error -1 is returned. */ +int linenoiseHistoryLoad(const char *filename) { + FILE *fp = fopen(filename,"r"); + char buf[LINENOISE_MAX_LINE]; + + if (fp == NULL) return -1; + + while (fgets(buf,LINENOISE_MAX_LINE,fp) != NULL) { + char *src, *dest; + + /* Decode backslash escaped values */ + for (src = dest = buf; *src; src++) { + char ch = *src; + + if (ch == '\\') { + src++; + if (*src == 'n') { + ch = '\n'; + } + else if (*src == 'r') { + ch = '\r'; + } else { + ch = *src; + } + } + *dest++ = ch; + } + /* Remove trailing newline */ + if (dest != buf && (dest[-1] == '\n' || dest[-1] == '\r')) { + dest--; + } + *dest = 0; + + linenoiseHistoryAdd(buf); + } + fclose(fp); + return 0; +} + +/* Provide access to the history buffer. + * + * If 'len' is not NULL, the length is stored in *len. + */ +char **linenoiseHistory(int *len) { + if (len) { + *len = history_len; + } + return history; +} diff --git a/src/linenoise/src/linenoise.h b/src/linenoise/src/linenoise.h new file mode 100644 index 0000000..ab0058a --- /dev/null +++ b/src/linenoise/src/linenoise.h @@ -0,0 +1,62 @@ +/* linenoise.h -- guerrilla line editing library against the idea that a + * line editing lib needs to be 20,000 lines of C code. + * + * See linenoise.c for more information. + * + * ------------------------------------------------------------------------ + * + * Copyright (c) 2010, Salvatore Sanfilippo <antirez at gmail dot com> + * Copyright (c) 2010, Pieter Noordhuis <pcnoordhuis at gmail dot com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __LINENOISE_H +#define __LINENOISE_H + +#ifndef NO_COMPLETION +typedef struct linenoiseCompletions { + size_t len; + char **cvec; +} linenoiseCompletions; + +typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); +void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); +void linenoiseAddCompletion(linenoiseCompletions *, const char *); +#endif + +typedef int(linenoiseCharacterCallback)(const char *, size_t, char); +void linenoiseSetCharacterCallback(linenoiseCharacterCallback *, char); + +char *linenoise(const char *prompt); +int linenoiseHistoryAdd(const char *line); +int linenoiseHistorySetMaxLen(int len); +int linenoiseHistorySave(const char *filename); +int linenoiseHistoryLoad(const char *filename); +void linenoiseHistoryFree(void); +char **linenoiseHistory(int *len); + +#endif /* __LINENOISE_H */ diff --git a/src/linenoise/src/utf8.c b/src/linenoise/src/utf8.c new file mode 100644 index 0000000..26924b4 --- /dev/null +++ b/src/linenoise/src/utf8.c @@ -0,0 +1,115 @@ +/** + * UTF-8 utility functions + * + * (c) 2010 Steve Bennett <steveb@workware.net.au> + * + * See LICENCE for licence details. + */ + +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include "utf8.h" + +#ifdef USE_UTF8 +int utf8_fromunicode(char *p, unsigned short uc) +{ + if (uc <= 0x7f) { + *p = uc; + return 1; + } + else if (uc <= 0x7ff) { + *p++ = 0xc0 | ((uc & 0x7c0) >> 6); + *p = 0x80 | (uc & 0x3f); + return 2; + } + else { + *p++ = 0xe0 | ((uc & 0xf000) >> 12); + *p++ = 0x80 | ((uc & 0xfc0) >> 6); + *p = 0x80 | (uc & 0x3f); + return 3; + } +} + +int utf8_charlen(int c) +{ + if ((c & 0x80) == 0) { + return 1; + } + if ((c & 0xe0) == 0xc0) { + return 2; + } + if ((c & 0xf0) == 0xe0) { + return 3; + } + if ((c & 0xf8) == 0xf0) { + return 4; + } + /* Invalid sequence */ + return -1; +} + +int utf8_strlen(const char *str, int bytelen) +{ + int charlen = 0; + if (bytelen < 0) { + bytelen = strlen(str); + } + while (bytelen) { + int c; + int l = utf8_tounicode(str, &c); + charlen++; + str += l; + bytelen -= l; + } + return charlen; +} + +int utf8_index(const char *str, int index) +{ + const char *s = str; + while (index--) { + int c; + s += utf8_tounicode(s, &c); + } + return s - str; +} + +int utf8_charequal(const char *s1, const char *s2) +{ + int c1, c2; + + utf8_tounicode(s1, &c1); + utf8_tounicode(s2, &c2); + + return c1 == c2; +} + +int utf8_tounicode(const char *str, int *uc) +{ + unsigned const char *s = (unsigned const char *)str; + + if (s[0] < 0xc0) { + *uc = s[0]; + return 1; + } + if (s[0] < 0xe0) { + if ((s[1] & 0xc0) == 0x80) { + *uc = ((s[0] & ~0xc0) << 6) | (s[1] & ~0x80); + return 2; + } + } + else if (s[0] < 0xf0) { + if (((str[1] & 0xc0) == 0x80) && ((str[2] & 0xc0) == 0x80)) { + *uc = ((s[0] & ~0xe0) << 12) | ((s[1] & ~0x80) << 6) | (s[2] & ~0x80); + return 3; + } + } + + /* Invalid sequence, so just return the byte */ + *uc = *s; + return 1; +} + +#endif diff --git a/src/linenoise/src/utf8.h b/src/linenoise/src/utf8.h new file mode 100644 index 0000000..525766d --- /dev/null +++ b/src/linenoise/src/utf8.h @@ -0,0 +1,79 @@ +#ifndef UTF8_UTIL_H +#define UTF8_UTIL_H +/** + * UTF-8 utility functions + * + * (c) 2010 Steve Bennett <steveb@workware.net.au> + * + * See LICENCE for licence details. + */ + +#ifndef USE_UTF8 +#include <ctype.h> + +/* No utf-8 support. 1 byte = 1 char */ +#define utf8_strlen(S, B) ((B) < 0 ? (int)strlen(S) : (B)) +#define utf8_tounicode(S, CP) (*(CP) = (unsigned char)*(S), 1) +#define utf8_index(C, I) (I) +#define utf8_charlen(C) 1 + +#else +/** + * Converts the given unicode codepoint (0 - 0xffff) to utf-8 + * and stores the result at 'p'. + * + * Returns the number of utf-8 characters (1-3). + */ +int utf8_fromunicode(char *p, unsigned short uc); + +/** + * Returns the length of the utf-8 sequence starting with 'c'. + * + * Returns 1-4, or -1 if this is not a valid start byte. + * + * Note that charlen=4 is not supported by the rest of the API. + */ +int utf8_charlen(int c); + +/** + * Returns the number of characters in the utf-8 + * string of the given byte length. + * + * Any bytes which are not part of an valid utf-8 + * sequence are treated as individual characters. + * + * The string *must* be null terminated. + * + * Does not support unicode code points > \uffff + */ +int utf8_strlen(const char *str, int bytelen); + +/** + * Returns the byte index of the given character in the utf-8 string. + * + * The string *must* be null terminated. + * + * This will return the byte length of a utf-8 string + * if given the char length. + */ +int utf8_index(const char *str, int charindex); + +/** + * Returns the unicode codepoint corresponding to the + * utf-8 sequence 'str'. + * + * Stores the result in *uc and returns the number of bytes + * consumed. + * + * If 'str' is null terminated, then an invalid utf-8 sequence + * at the end of the string will be returned as individual bytes. + * + * If it is not null terminated, the length *must* be checked first. + * + * Does not support unicode code points > \uffff + */ +int utf8_tounicode(const char *str, int *uc); + +#endif + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..45ddeb6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,107 @@ +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include "consts.h" +#include "utils.h" +#include "env.h" +#include "phantom.h" +#include "crashdump.h" + +#include <QApplication> +#include <QSslSocket> +#include <QIcon> + +#include <exception> +#include <stdio.h> + +static int inner_main(int argc, char** argv) +{ + QApplication app(argc, argv); + + app.setWindowIcon(QIcon(":/phantomjs-icon.png")); + app.setApplicationName("PhantomJS"); + app.setOrganizationName("Ofi Labs"); + app.setOrganizationDomain("www.ofilabs.com"); + app.setApplicationVersion(PHANTOMJS_VERSION_STRING); + + // Registering an alternative Message Handler + qInstallMessageHandler(Utils::messageHandler); + +#if defined(Q_OS_LINUX) + if (QSslSocket::supportsSsl()) { + // Don't perform on-demand loading of root certificates on Linux + QSslSocket::addDefaultCaCertificates(QSslSocket::systemCaCertificates()); + } +#endif + + // Get the Phantom singleton + Phantom* phantom = Phantom::instance(); + + // Start script execution + if (phantom->execute()) { + app.exec(); + } + + // End script execution: delete the phantom singleton and set + // execution return value + int retVal = phantom->returnValue(); + delete phantom; + return retVal; +} + +int main(int argc, char** argv) +{ + try { + init_crash_handler(); + return inner_main(argc, argv); + + // These last-ditch exception handlers write to the C stderr + // because who knows what kind of state Qt is in. And they avoid + // using fprintf because _that_ might be in bad shape too. + // (I would drop all the way down to write() but then I'd have to + // write the code again for Windows.) + // + // print_crash_message includes a call to fflush(stderr). + } catch (std::bad_alloc) { + fputs("Memory exhausted.\n", stderr); + fflush(stderr); + return 1; + + } catch (std::exception& e) { + fputs("Uncaught C++ exception: ", stderr); + fputs(e.what(), stderr); + putc('\n', stderr); + print_crash_message(); + return 1; + + } catch (...) { + fputs("Uncaught nonstandard exception.\n", stderr); + print_crash_message(); + return 1; + } +} diff --git a/src/modules/child_process.js b/src/modules/child_process.js new file mode 100644 index 0000000..6fcaaea --- /dev/null +++ b/src/modules/child_process.js @@ -0,0 +1,165 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2012 execjosh, http://execjosh.blogspot.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var NOP = function () {} + +/** + * spawn(command, [args], [options]) + */ +exports.spawn = function (cmd, args, opts) { + var ctx = newContext() + + if (null == opts) { + opts = {} + } + + opts.encoding = opts.encoding || "utf8" + ctx._setEncoding(opts.encoding) + + ctx._start(cmd, args) + + return ctx +} + +/** + * exec(command, [options], callback) + */ +exports.exec = function (cmd, opts, cb) { + if (null == cb) { + cb = NOP + } + + return cb(new Error("NotYetImplemented")) +} + +/** + * execFile(file, args, options, callback) + */ +exports.execFile = function (cmd, args, opts, cb) { + var ctx = newContext() + + if (null == cb) { + cb = NOP + } + + if (null == opts) { + opts = {} + } + + opts.encoding = opts.encoding || "utf8" + ctx._setEncoding(opts.encoding) + + var stdout = "" + ctx.stdout.on("data", function (chunk) { + stdout += chunk + }) + + var stderr = "" + ctx.stderr.on("data", function (chunk) { + stderr += chunk + }) + + ctx.on("exit", function (code) { + return cb(null, stdout, stderr) + }) + + ctx._start(cmd, args) + + return ctx +} + +/** + * fork(modulePath, [args], [options]) + */ +exports.fork = function (modulePath, args, opts) { + throw new Error("NotYetImplemented") +} + + +// private + +function newContext() { + var ctx = exports._createChildProcessContext() + + // TODO: "Buffer" the signals and redispatch them? + + ctx.on = function (evt, cb) { + var handler + + switch (evt) { + case "exit": + handler = ctx[evt] + break + default: + break + } + + // Connect the callback to the signal + if (isFunction(handler)) { + handler.connect(cb) + } + } + + ctx.stdout = new FakeReadableStream("stdout") + ctx.stderr = new FakeReadableStream("stderr") + + // Emulates `Readable Stream` + function FakeReadableStream(streamName) { + this.on = function (evt, cb) { + switch (evt) { + case 'data': + ctx[streamName + "Data"].connect(cb) + break + default: + break + } + } + } + + return ctx +} + +function delayCallback() { + var args = 0 < arguments.length ? [].slice.call(arguments, 0) : [] + var fn = args.shift() + if (!isFunc(fn)) { + return + } + var that = this + setTimeout(function () { + fn.apply(that, args) + }, 0) +} + +function isFunction(o) { + return typeof o === 'function' +} diff --git a/src/modules/cookiejar.js b/src/modules/cookiejar.js new file mode 100644 index 0000000..2c1f5be --- /dev/null +++ b/src/modules/cookiejar.js @@ -0,0 +1,50 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2013 Joseph Rollinson, jtrollinson@gmail.com + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* Only there for backwards compatibility. */ +function decorateCookieJar(jar) { + return jar; +} + +/* Creates and decorates a new cookie jar. + * path is the file path where Phantomjs will store the cookie jar persistently. + * path is not mandatory. + */ +exports.create = function (path) { + if (arguments.length < 1) { + path = ""; + } + return decorateCookieJar(phantom.createCookieJar(path)); +}; + +/* Exports the decorateCookieJar function */ +exports.decorate = decorateCookieJar; diff --git a/src/modules/fs.js b/src/modules/fs.js new file mode 100644 index 0000000..0685dda --- /dev/null +++ b/src/modules/fs.js @@ -0,0 +1,268 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// JavaScript "shim" to throw exceptions in case a critical operation fails. + +/** Convert a modeOrOpts to a map + * + * @param modeOrOpts + * mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters. + * opts: Options. + * - mode (see Open Mode above) + * - charset An IANA, case insensitive, charset name. + */ +function modeOrOptsToOpts(modeOrOpts) { + var opts; + + // Extract charset from opts + if (modeOrOpts == null) { + // Empty options + opts = {}; + } else if (typeof modeOrOpts !== 'object') { + opts = { + mode: modeOrOpts + }; + } else { + opts = modeOrOpts; + } + + return opts; +} + +/** Open and return a "file" object. + * It will throw exception if it fails. + * + * @param path Path of the file to open + * @param modeOrOpts + * mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters. + * opts: Options. + * - mode (see Open Mode above) + * - charset An IANA, case insensitive, charset name. + * @return "file" object + */ +exports.open = function (path, modeOrOpts) { + // Open file + var file = exports._open(path, modeOrOptsToOpts(modeOrOpts)); + if (file) { + return file; + } + throw "Unable to open file '" + path + "'"; +}; + +/** Open, read and return text content of a file. + * It will throw an exception if it fails. + * + * @param path Path of the file to read from + * @param modeOrOpts + * mode: Open Mode. 'b' to open a raw binary file + * opts: Options. + * - mode (see Open Mode above) + * - charset An IANA, case insensitive, charset name. + * @return file content + */ +exports.read = function (path, modeOrOpts) { + if (typeof modeOrOpts == 'string') { + if (modeOrOpts.toLowerCase() == 'b') { + // open binary + modeOrOpts = {mode: modeOrOpts}; + } else { + // asume charset is given + modeOrOpts = {charset: modeOrOpts}; + } + } + var opts = modeOrOptsToOpts(modeOrOpts); + // ensure we open for reading + if ( typeof opts.mode !== 'string' ) { + opts.mode = 'r'; + } else if ( opts.mode.indexOf('r') == -1 ) { + opts.mode += 'r'; + } + var f = exports.open(path, opts), + content = f.read(); + + f.close(); + return content; +}; + +/** Open and write text content to a file + * It will throw an exception if it fails. + * + * @param path Path of the file to read from + * @param content Content to write to the file + * @param modeOrOpts + * mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters. + * opts: Options. + * - mode (see Open Mode above) + * - charset An IANA, case insensitive, charset name. + */ +exports.write = function (path, content, modeOrOpts) { + var opts = modeOrOptsToOpts(modeOrOpts); + // ensure we open for writing + if ( typeof opts.mode !== 'string' ) { + opts.mode = 'w'; + } else if ( opts.mode.indexOf('w') == -1 ) { + opts.mode += 'w'; + } + var f = exports.open(path, opts); + + f.write(content); + f.close(); +}; + +/** Return the size of a file, in bytes. + * It will throw an exception if it fails. + * + * @param path Path of the file to read the size of + * @return File size in bytes + */ +exports.size = function (path) { + var size = exports._size(path); + if (size !== -1) { + return size; + } + throw "Unable to read file '" + path + "' size"; +}; + +/** Copy a file. + * It will throw an exception if it fails. + * + * @param source Path of the source file + * @param destination Path of the destination file + */ +exports.copy = function (source, destination) { + if (!exports._copy(source, destination)) { + throw "Unable to copy file '" + source + "' at '" + destination + "'"; + } +}; + +/** Copy a directory tree. + * It will throw an exception if it fails. + * + * @param source Path of the source directory tree + * @param destination Path of the destination directory tree + */ +exports.copyTree = function (source, destination) { + if (!exports._copyTree(source, destination)) { + throw "Unable to copy directory tree '" + source + "' at '" + destination + "'"; + } +}; + +/** Move a file. + * It will throw an exception if it fails. + * + * @param source Path of the source file + * @param destination Path of the destination file + */ +exports.move = function (source, destination) { + exports.copy(source, destination); + exports.remove(source); +}; + +/** Removes a file. + * It will throw an exception if it fails. + * + * @param path Path of the file to remove + */ +exports.remove = function (path) { + if (!exports._remove(path)) { + throw "Unable to remove file '" + path + "'"; + } +}; + +/** Removes a directory. + * It will throw an exception if it fails. + * + * @param path Path of the directory to remove + */ +exports.removeDirectory = function (path) { + if (!exports._removeDirectory(path)) { + throw "Unable to remove directory '" + path + "'"; + } +}; + +/** Removes a directory tree. + * It will throw an exception if it fails. + * + * @param path Path of the directory tree to remove + */ +exports.removeTree = function (path) { + if (!exports._removeTree(path)) { + throw "Unable to remove directory tree '" + path + "'"; + } +}; + +exports.touch = function (path) { + exports.write(path, "", 'a'); +}; + +// Path stuff + +exports.join = function() { + var args = []; + + if (0 < arguments.length) { + args = args.slice.call(arguments, 0) + // Make sure each part is a string and remove empty parts (except at begining) + .map(function (part, idx) { + if (null != part) { + var str = part.toString(); + if ("" === str) { + return 0 === idx ? "" : null; + } else { + return str; + } + } + }) + // Remove empty parts + .filter(function (part) { + return null != part; + }); + } + + var ret = args.join("/"); + + return 0 < ret.length ? ret : "."; +}; + +exports.split = function (path) { + if (typeof path !== "string") { + return []; + } + + return exports.fromNativeSeparators(path) + // Collapse redundant separators + .replace(/\/+/g, "/") + // Remove separator at end + .replace(/\/$/, "") + // And split + .split("/") +}; diff --git a/src/modules/system.js b/src/modules/system.js new file mode 100644 index 0000000..fef9103 --- /dev/null +++ b/src/modules/system.js @@ -0,0 +1,30 @@ +/* + * CommonJS System/1.0 + * Spec: http://wiki.commonjs.org/wiki/System/1.0 + */ + +exports.platform = 'phantomjs'; + +Object.defineProperty(exports, 'stdout', { + enumerable: true, + writeable: false, + get: function() { + return exports.standardout; + } +}); + +Object.defineProperty(exports, 'stdin', { + enumerable: true, + writeable: false, + get: function() { + return exports.standardin; + } +}); + +Object.defineProperty(exports, 'stderr', { + enumerable: true, + writeable: false, + get: function() { + return exports.standarderr; + } +}); diff --git a/src/modules/webpage.js b/src/modules/webpage.js new file mode 100644 index 0000000..20eae9a --- /dev/null +++ b/src/modules/webpage.js @@ -0,0 +1,893 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true,phantom:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com> + Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com> + Copyright (C) 2011 James Roe <roejames12@hotmail.com> + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + Copyright (C) 2012 James M. Greene <james.m.greene@gmail.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +function checkType(o, type) { + return typeof o === type; +} + +function isObject(o) { + return checkType(o, 'object'); +} + +function isUndefined(o) { + return checkType(o, 'undefined'); +} + +function isUndefinedOrNull(o) { + return isUndefined(o) || null === o; +} + +function copyInto(target, source) { + if (target === source || isUndefinedOrNull(source)) { + return target; + } + + target = target || {}; + + // Copy into objects only + if (isObject(target)) { + // Make sure source exists + source = source || {}; + + if (isObject(source)) { + var i, newTarget, newSource; + for (i in source) { + if (source.hasOwnProperty(i)) { + newTarget = target[i]; + newSource = source[i]; + + if (newTarget && isObject(newSource)) { + // Deep copy + newTarget = copyInto(target[i], newSource); + } else { + newTarget = newSource; + } + + if (!isUndefined(newTarget)) { + target[i] = newTarget; + } + } + } + } else { + target = source; + } + } + + return target; +} + +function definePageSignalHandler(page, handlers, handlerName, signalName) { + Object.defineProperty(page, handlerName, { + set: function (f) { + // Disconnect previous handler (if any) + if (!!handlers[handlerName] && typeof handlers[handlerName].callback === "function") { + try { + this[signalName].disconnect(handlers[handlerName].callback); + } catch (e) {} + } + + // Delete the previous handler + delete handlers[handlerName]; + + // Connect the new handler iff it's a function + if (typeof f === "function") { + // Store the new handler for reference + handlers[handlerName] = { + callback: f + } + this[signalName].connect(f); + } + }, + get: function() { + return !!handlers[handlerName] && typeof handlers[handlerName].callback === "function" ? + handlers[handlerName].callback : + undefined; + } + }); +} + +function definePageCallbackHandler(page, handlers, handlerName, callbackConstructor) { + Object.defineProperty(page, handlerName, { + set: function(f) { + // Fetch the right callback object + var callbackObj = page[callbackConstructor](); + + // Disconnect previous handler (if any) + var handlerObj = handlers[handlerName]; + if (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") { + try { + callbackObj.called.disconnect(handlerObj.connector); + } catch (e) { + console.log(e); + } + } + + // Delete the previous handler + delete handlers[handlerName]; + + // Connect the new handler iff it's a function + if (typeof f === "function") { + var connector = function() { + // Callback will receive a "deserialized", normal "arguments" array + callbackObj.returnValue = f.apply(this, arguments[0]); + }; + + // Store the new handler for reference + handlers[handlerName] = { + callback: f, + connector: connector + }; + + // Connect a new handler + callbackObj.called.connect(connector); + } + }, + get: function() { + var handlerObj = handlers[handlerName]; + return (!!handlerObj && typeof handlerObj.callback === "function" && typeof handlerObj.connector === "function") ? + handlers[handlerName].callback : + undefined; + } + }); +} + +// Inspired by Douglas Crockford's remedies: proper String quoting. +// @see http://javascript.crockford.com/remedial.html +function quoteString(str) { + var c, i, l = str.length, o = '"'; + for (i = 0; i < l; i += 1) { + c = str.charAt(i); + if (c >= ' ') { + if (c === '\\' || c === '"') { + o += '\\'; + } + o += c; + } else { + switch (c) { + case '\b': + o += '\\b'; + break; + case '\f': + o += '\\f'; + break; + case '\n': + o += '\\n'; + break; + case '\r': + o += '\\r'; + break; + case '\t': + o += '\\t'; + break; + default: + c = c.charCodeAt(); + o += '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + } + } + } + return o + '"'; +} + +// Inspired by Douglas Crockford's remedies: a better Type Detection. +// @see http://javascript.crockford.com/remedial.html +function detectType(value) { + var s = typeof value; + if (s === 'object') { + if (value) { + if (value instanceof Array) { + s = 'array'; + } else if (value instanceof RegExp) { + s = 'regexp'; + } else if (value instanceof Date) { + s = 'date'; + } + } else { + s = 'null'; + } + } + return s; +} + +function decorateNewPage(opts, page) { + var handlers = {}; + + try { + page.rawPageCreated.connect(function(newPage) { + // Decorate the new raw page appropriately + newPage = decorateNewPage(opts, newPage); + + // Notify via callback, if a callback was provided + if (page.onPageCreated && typeof(page.onPageCreated) === "function") { + page.onPageCreated(newPage); + } + }); + } catch (e) {} + + // deep copy + page.settings = JSON.parse(JSON.stringify(phantom.defaultPageSettings)); + + definePageSignalHandler(page, handlers, "onInitialized", "initialized"); + + definePageSignalHandler(page, handlers, "onLoadStarted", "loadStarted"); + + definePageSignalHandler(page, handlers, "onLoadFinished", "loadFinished"); + + definePageSignalHandler(page, handlers, "onUrlChanged", "urlChanged"); + + definePageSignalHandler(page, handlers, "onNavigationRequested", "navigationRequested"); + + definePageSignalHandler(page, handlers, "onRepaintRequested", "repaintRequested"); + + definePageSignalHandler(page, handlers, "onResourceRequested", "resourceRequested"); + + definePageSignalHandler(page, handlers, "onResourceReceived", "resourceReceived"); + + definePageSignalHandler(page, handlers, "onResourceError", "resourceError"); + + definePageSignalHandler(page, handlers, "onResourceTimeout", "resourceTimeout"); + + definePageSignalHandler(page, handlers, "onAlert", "javaScriptAlertSent"); + + definePageSignalHandler(page, handlers, "onConsoleMessage", "javaScriptConsoleMessageSent"); + + definePageSignalHandler(page, handlers, "onClosing", "closing"); + + // Private callback for "page.open()" + definePageSignalHandler(page, handlers, "_onPageOpenFinished", "loadFinished"); + + phantom.__defineErrorSignalHandler__(page, page, handlers); + + page.onError = phantom.defaultErrorHandler; + + page.open = function (url, arg1, arg2, arg3, arg4) { + var thisPage = this; + + if (arguments.length === 1) { + this.openUrl(url, 'get', this.settings); + return; + } else if (arguments.length === 2 && typeof arg1 === 'function') { + this._onPageOpenFinished = function() { + thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) + arg1.apply(thisPage, arguments); //< Invoke the actual callback + } + this.openUrl(url, 'get', this.settings); + return; + } else if (arguments.length === 2) { + this.openUrl(url, arg1, this.settings); + return; + } else if (arguments.length === 3 && typeof arg2 === 'function') { + this._onPageOpenFinished = function() { + thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) + arg2.apply(thisPage, arguments); //< Invoke the actual callback + } + this.openUrl(url, arg1, this.settings); + return; + } else if (arguments.length === 3) { + this.openUrl(url, { + operation: arg1, + data: arg2 + }, this.settings); + return; + } else if (arguments.length === 4) { + this._onPageOpenFinished = function() { + thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) + arg3.apply(thisPage, arguments); //< Invoke the actual callback + } + this.openUrl(url, { + operation: arg1, + data: arg2 + }, this.settings); + return; + } else if (arguments.length === 5) { + this._onPageOpenFinished = function() { + thisPage._onPageOpenFinished = null; //< Disconnect callback (should fire only once) + arg4.apply(thisPage, arguments); //< Invoke the actual callback + } + this.openUrl(url, { + operation: arg1, + data: arg2, + headers : arg3 + }, this.settings); + return; + } + throw "Wrong use of WebPage#open"; + }; + + /** + * Include an external JavaScript file and notify when done. + * @param scriptUrl URL to the Script to include + * @param onScriptLoaded If provided, this call back is executed when the inclusion is done + */ + page.includeJs = function (scriptUrl, onScriptLoaded) { + // Register temporary signal handler for 'alert()' + var self = this; + function alertCallback (msgFromAlert) { + if (msgFromAlert === scriptUrl) { + // Resource loaded, time to fire the callback (if any) + if (onScriptLoaded && typeof(onScriptLoaded) === "function") { + onScriptLoaded(scriptUrl); + } + // And disconnect the signal handler + try { + self.javaScriptAlertSent.disconnect(alertCallback); + } catch (e) {} + } + } + self.javaScriptAlertSent.connect(alertCallback); + + // Append the script tag to the body + self._appendScriptElement(scriptUrl); + }; + + /** + * evaluate a function in the page + * @param {function} func the function to evaluate + * @param {...} args function arguments + * @return {*} the function call result + */ + page.evaluate = function (func, args) { + var str, arg, argType, i, l; + if (!(func instanceof Function || typeof func === 'string' || func instanceof String)) { + throw "Wrong use of WebPage#evaluate"; + } + str = 'function() { return (' + func.toString() + ')('; + for (i = 1, l = arguments.length; i < l; i++) { + arg = arguments[i]; + argType = detectType(arg); + + switch (argType) { + case "object": //< for type "object" + case "array": //< for type "array" + str += JSON.stringify(arg) + "," + break; + case "date": //< for type "date" + str += "new Date(" + JSON.stringify(arg) + ")," + break; + case "string": //< for type "string" + str += quoteString(arg) + ','; + break; + default: // for types: "null", "number", "function", "regexp", "undefined" + str += arg + ','; + break; + } + } + str = str.replace(/,$/, '') + '); }'; + return this.evaluateJavaScript(str); + }; + + /** + * evaluate a function in the page, asynchronously + * NOTE: it won't return anything: the execution is asynchronous respect to the call. + * NOTE: the execution stack starts from within the page object + * @param {function} func the function to evaluate + * @param {number} timeMs time to wait before execution + * @param {...} args function arguments + */ + page.evaluateAsync = function (func, timeMs, args) { + // Remove the first 2 arguments because we are going to consume them + var args = Array.prototype.slice.call(arguments, 2), + numArgsToAppend = args.length, + funcTimeoutWrapper; + + if (!(func instanceof Function || typeof func === 'string' || func instanceof String)) { + throw "Wrong use of WebPage#evaluateAsync"; + } + // Wrapping the "func" argument into a setTimeout + funcTimeoutWrapper = "function() { setTimeout(" + func.toString() + ", " + timeMs; + while(numArgsToAppend > 0) { + --numArgsToAppend; + funcTimeoutWrapper += ", arguments[" + numArgsToAppend + "]"; + } + funcTimeoutWrapper += "); }"; + + // Augment the "args" array + args.splice(0, 0, funcTimeoutWrapper); + + this.evaluate.apply(this, args); + }; + + /** + * upload a file + * @param {string} selector css selector for the file input element + * @param {string,array} fileNames the name(s) of the file(s) to upload + */ + page.uploadFile = function(selector, fileNames) { + if (typeof fileNames == "string") { + fileNames = [fileNames]; + } + + this._uploadFile(selector, fileNames); + }; + + // Copy options into page + if (opts) { + page = copyInto(page, opts); + } + + // Calls from within the page to "phantomCallback()" arrive to this handler + definePageCallbackHandler(page, handlers, "onCallback", "_getGenericCallback"); + + // Calls arrive to this handler when the user is asked to pick a file + definePageCallbackHandler(page, handlers, "onFilePicker", "_getFilePickerCallback"); + + // Calls from within the page to "window.confirm(message)" arrive to this handler + // @see https://developer.mozilla.org/en/DOM/window.confirm + definePageCallbackHandler(page, handlers, "onConfirm", "_getJsConfirmCallback"); + + // Calls from within the page to "window.prompt(message, defaultValue)" arrive to this handler + // @see https://developer.mozilla.org/en/DOM/window.prompt + definePageCallbackHandler(page, handlers, "onPrompt", "_getJsPromptCallback"); + + // Calls from within the page when some javascript code running to long + definePageCallbackHandler(page, handlers, "onLongRunningScript", "_getJsInterruptCallback"); + + page.event = {}; + page.event.modifier = { + shift: 0x02000000, + ctrl: 0x04000000, + alt: 0x08000000, + meta: 0x10000000, + keypad: 0x20000000 + }; + + page.event.key = { + '0': 48, + '1': 49, + '2': 50, + '3': 51, + '4': 52, + '5': 53, + '6': 54, + '7': 55, + '8': 56, + '9': 57, + 'A': 65, + 'AE': 198, + 'Aacute': 193, + 'Acircumflex': 194, + 'AddFavorite': 16777408, + 'Adiaeresis': 196, + 'Agrave': 192, + 'Alt': 16777251, + 'AltGr': 16781571, + 'Ampersand': 38, + 'Any': 32, + 'Apostrophe': 39, + 'ApplicationLeft': 16777415, + 'ApplicationRight': 16777416, + 'Aring': 197, + 'AsciiCircum': 94, + 'AsciiTilde': 126, + 'Asterisk': 42, + 'At': 64, + 'Atilde': 195, + 'AudioCycleTrack': 16777478, + 'AudioForward': 16777474, + 'AudioRandomPlay': 16777476, + 'AudioRepeat': 16777475, + 'AudioRewind': 16777413, + 'Away': 16777464, + 'B': 66, + 'Back': 16777313, + 'BackForward': 16777414, + 'Backslash': 92, + 'Backspace': 16777219, + 'Backtab': 16777218, + 'Bar': 124, + 'BassBoost': 16777331, + 'BassDown': 16777333, + 'BassUp': 16777332, + 'Battery': 16777470, + 'Bluetooth': 16777471, + 'Book': 16777417, + 'BraceLeft': 123, + 'BraceRight': 125, + 'BracketLeft': 91, + 'BracketRight': 93, + 'BrightnessAdjust': 16777410, + 'C': 67, + 'CD': 16777418, + 'Calculator': 16777419, + 'Calendar': 16777444, + 'Call': 17825796, + 'Camera': 17825824, + 'CameraFocus': 17825825, + 'Cancel': 16908289, + 'CapsLock': 16777252, + 'Ccedilla': 199, + 'Clear': 16777227, + 'ClearGrab': 16777421, + 'Close': 16777422, + 'Codeinput': 16781623, + 'Colon': 58, + 'Comma': 44, + 'Community': 16777412, + 'Context1': 17825792, + 'Context2': 17825793, + 'Context3': 17825794, + 'Context4': 17825795, + 'ContrastAdjust': 16777485, + 'Control': 16777249, + 'Copy': 16777423, + 'Cut': 16777424, + 'D': 68, + 'DOS': 16777426, + 'Dead_Abovedot': 16781910, + 'Dead_Abovering': 16781912, + 'Dead_Acute': 16781905, + 'Dead_Belowdot': 16781920, + 'Dead_Breve': 16781909, + 'Dead_Caron': 16781914, + 'Dead_Cedilla': 16781915, + 'Dead_Circumflex': 16781906, + 'Dead_Diaeresis': 16781911, + 'Dead_Doubleacute': 16781913, + 'Dead_Grave': 16781904, + 'Dead_Hook': 16781921, + 'Dead_Horn': 16781922, + 'Dead_Iota': 16781917, + 'Dead_Macron': 16781908, + 'Dead_Ogonek': 16781916, + 'Dead_Semivoiced_Sound': 16781919, + 'Dead_Tilde': 16781907, + 'Dead_Voiced_Sound': 16781918, + 'Delete': 16777223, + 'Direction_L': 16777305, + 'Direction_R': 16777312, + 'Display': 16777425, + 'Documents': 16777427, + 'Dollar': 36, + 'Down': 16777237, + 'E': 69, + 'ETH': 208, + 'Eacute': 201, + 'Ecircumflex': 202, + 'Ediaeresis': 203, + 'Egrave': 200, + 'Eisu_Shift': 16781615, + 'Eisu_toggle': 16781616, + 'Eject': 16777401, + 'End': 16777233, + 'Enter': 16777221, + 'Equal': 61, + 'Escape': 16777216, + 'Excel': 16777428, + 'Exclam': 33, + 'Execute': 16908291, + 'Explorer': 16777429, + 'F': 70, + 'F1': 16777264, + 'F10': 16777273, + 'F11': 16777274, + 'F12': 16777275, + 'F13': 16777276, + 'F14': 16777277, + 'F15': 16777278, + 'F16': 16777279, + 'F17': 16777280, + 'F18': 16777281, + 'F19': 16777282, + 'F2': 16777265, + 'F20': 16777283, + 'F21': 16777284, + 'F22': 16777285, + 'F23': 16777286, + 'F24': 16777287, + 'F25': 16777288, + 'F26': 16777289, + 'F27': 16777290, + 'F28': 16777291, + 'F29': 16777292, + 'F3': 16777266, + 'F30': 16777293, + 'F31': 16777294, + 'F32': 16777295, + 'F33': 16777296, + 'F34': 16777297, + 'F35': 16777298, + 'F4': 16777267, + 'F5': 16777268, + 'F6': 16777269, + 'F7': 16777270, + 'F8': 16777271, + 'F9': 16777272, + 'Favorites': 16777361, + 'Finance': 16777411, + 'Flip': 17825798, + 'Forward': 16777314, + 'G': 71, + 'Game': 16777430, + 'Go': 16777431, + 'Greater': 62, + 'H': 72, + 'Hangul': 16781617, + 'Hangul_Banja': 16781625, + 'Hangul_End': 16781619, + 'Hangul_Hanja': 16781620, + 'Hangul_Jamo': 16781621, + 'Hangul_Jeonja': 16781624, + 'Hangul_PostHanja': 16781627, + 'Hangul_PreHanja': 16781626, + 'Hangul_Romaja': 16781622, + 'Hangul_Special': 16781631, + 'Hangul_Start': 16781618, + 'Hangup': 17825797, + 'Hankaku': 16781609, + 'Help': 16777304, + 'Henkan': 16781603, + 'Hibernate': 16777480, + 'Hiragana': 16781605, + 'Hiragana_Katakana': 16781607, + 'History': 16777407, + 'Home': 16777232, + 'HomePage': 16777360, + 'HotLinks': 16777409, + 'Hyper_L': 16777302, + 'Hyper_R': 16777303, + 'I': 73, + 'Iacute': 205, + 'Icircumflex': 206, + 'Idiaeresis': 207, + 'Igrave': 204, + 'Insert': 16777222, + 'J': 74, + 'K': 75, + 'Kana_Lock': 16781613, + 'Kana_Shift': 16781614, + 'Kanji': 16781601, + 'Katakana': 16781606, + 'KeyboardBrightnessDown': 16777398, + 'KeyboardBrightnessUp': 16777397, + 'KeyboardLightOnOff': 16777396, + 'L': 76, + 'LastNumberRedial': 17825801, + 'Launch0': 16777378, + 'Launch1': 16777379, + 'Launch2': 16777380, + 'Launch3': 16777381, + 'Launch4': 16777382, + 'Launch5': 16777383, + 'Launch6': 16777384, + 'Launch7': 16777385, + 'Launch8': 16777386, + 'Launch9': 16777387, + 'LaunchA': 16777388, + 'LaunchB': 16777389, + 'LaunchC': 16777390, + 'LaunchD': 16777391, + 'LaunchE': 16777392, + 'LaunchF': 16777393, + 'LaunchG': 16777486, + 'LaunchH': 16777487, + 'LaunchMail': 16777376, + 'LaunchMedia': 16777377, + 'Left': 16777234, + 'Less': 60, + 'LightBulb': 16777405, + 'LogOff': 16777433, + 'M': 77, + 'MailForward': 16777467, + 'Market': 16777434, + 'Massyo': 16781612, + 'MediaLast': 16842751, + 'MediaNext': 16777347, + 'MediaPause': 16777349, + 'MediaPlay': 16777344, + 'MediaPrevious': 16777346, + 'MediaRecord': 16777348, + 'MediaStop': 16777345, + 'MediaTogglePlayPause': 16777350, + 'Meeting': 16777435, + 'Memo': 16777404, + 'Menu': 16777301, + 'MenuKB': 16777436, + 'MenuPB': 16777437, + 'Messenger': 16777465, + 'Meta': 16777250, + 'Minus': 45, + 'Mode_switch': 16781694, + 'MonBrightnessDown': 16777395, + 'MonBrightnessUp': 16777394, + 'Muhenkan': 16781602, + 'Multi_key': 16781600, + 'MultipleCandidate': 16781629, + 'Music': 16777469, + 'MySites': 16777438, + 'N': 78, + 'News': 16777439, + 'No': 16842754, + 'Ntilde': 209, + 'NumLock': 16777253, + 'NumberSign': 35, + 'O': 79, + 'Oacute': 211, + 'Ocircumflex': 212, + 'Odiaeresis': 214, + 'OfficeHome': 16777440, + 'Ograve': 210, + 'Ooblique': 216, + 'OpenUrl': 16777364, + 'Option': 16777441, + 'Otilde': 213, + 'P': 80, + 'PageDown': 16777239, + 'PageUp': 16777238, + 'ParenLeft': 40, + 'ParenRight': 41, + 'Paste': 16777442, + 'Pause': 16777224, + 'Percent': 37, + 'Period': 46, + 'Phone': 16777443, + 'Pictures': 16777468, + 'Play': 16908293, + 'Plus': 43, + 'PowerDown': 16777483, + 'PowerOff': 16777399, + 'PreviousCandidate': 16781630, + 'Print': 16777225, + 'Printer': 16908290, + 'Q': 81, + 'Question': 63, + 'QuoteDbl': 34, + 'QuoteLeft': 96, + 'R': 82, + 'Refresh': 16777316, + 'Reload': 16777446, + 'Reply': 16777445, + 'Return': 16777220, + 'Right': 16777236, + 'Romaji': 16781604, + 'RotateWindows': 16777447, + 'RotationKB': 16777449, + 'RotationPB': 16777448, + 'S': 83, + 'Save': 16777450, + 'ScreenSaver': 16777402, + 'ScrollLock': 16777254, + 'Search': 16777362, + 'Select': 16842752, + 'Semicolon': 59, + 'Send': 16777451, + 'Shift': 16777248, + 'Shop': 16777406, + 'SingleCandidate': 16781628, + 'Slash': 47, + 'Sleep': 16908292, + 'Space': 32, + 'Spell': 16777452, + 'SplitScreen': 16777453, + 'Standby': 16777363, + 'Stop': 16777315, + 'Subtitle': 16777477, + 'Super_L': 16777299, + 'Super_R': 16777300, + 'Support': 16777454, + 'Suspend': 16777484, + 'SysReq': 16777226, + 'T': 84, + 'THORN': 222, + 'Tab': 16777217, + 'TaskPane': 16777455, + 'Terminal': 16777456, + 'Time': 16777479, + 'ToDoList': 16777420, + 'ToggleCallHangup': 17825799, + 'Tools': 16777457, + 'TopMenu': 16777482, + 'Touroku': 16781611, + 'Travel': 16777458, + 'TrebleDown': 16777335, + 'TrebleUp': 16777334, + 'U': 85, + 'UWB': 16777473, + 'Uacute': 218, + 'Ucircumflex': 219, + 'Udiaeresis': 220, + 'Ugrave': 217, + 'Underscore': 95, + 'Up': 16777235, + 'V': 86, + 'Video': 16777459, + 'View': 16777481, + 'VoiceDial': 17825800, + 'VolumeDown': 16777328, + 'VolumeMute': 16777329, + 'VolumeUp': 16777330, + 'W': 87, + 'WLAN': 16777472, + 'WWW': 16777403, + 'WakeUp': 16777400, + 'WebCam': 16777466, + 'Word': 16777460, + 'X': 88, + 'Xfer': 16777461, + 'Y': 89, + 'Yacute': 221, + 'Yes': 16842753, + 'Z': 90, + 'Zenkaku': 16781608, + 'Zenkaku_Hankaku': 16781610, + 'Zoom': 16908294, + 'ZoomIn': 16777462, + 'ZoomOut': 16777463, + 'acute': 180, + 'brokenbar': 166, + 'cedilla': 184, + 'cent': 162, + 'copyright': 169, + 'currency': 164, + 'degree': 176, + 'diaeresis': 168, + 'division': 247, + 'exclamdown': 161, + 'guillemotleft': 171, + 'guillemotright': 187, + 'hyphen': 173, + 'iTouch': 16777432, + 'macron': 175, + 'masculine': 186, + 'mu': 181, + 'multiply': 215, + 'nobreakspace': 160, + 'notsign': 172, + 'onehalf': 189, + 'onequarter': 188, + 'onesuperior': 185, + 'ordfeminine': 170, + 'paragraph': 182, + 'periodcentered': 183, + 'plusminus': 177, + 'questiondown': 191, + 'registered': 174, + 'section': 167, + 'ssharp': 223, + 'sterling': 163, + 'threequarters': 190, + 'threesuperior': 179, + 'twosuperior': 178, + 'unknown': 33554431, + 'ydiaeresis': 255, + 'yen': 165 + }; + + return page; +} + +exports.create = function (opts) { + return decorateNewPage(opts, phantom.createWebPage()); +}; diff --git a/src/modules/webserver.js b/src/modules/webserver.js new file mode 100644 index 0000000..87fbe7e --- /dev/null +++ b/src/modules/webserver.js @@ -0,0 +1,132 @@ +/*jslint sloppy: true, nomen: true */ +/*global exports:true,phantom:true */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com> + Copyright (C) 2011 Ivan De Marino <ivan.de.marino@gmail.com> + Copyright (C) 2011 James Roe <roejames12@hotmail.com> + Copyright (C) 2011 execjosh, http://execjosh.blogspot.com + Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Author: Milian Wolff <milian.wolff@kdab.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +exports.create = function (opts) { + var server = phantom.createWebServer(), + handlers = {}; + + function checkType(o, type) { + return typeof o === type; + } + + function isObject(o) { + return checkType(o, 'object'); + } + + function isUndefined(o) { + return checkType(o, 'undefined'); + } + + function isUndefinedOrNull(o) { + return isUndefined(o) || null === o; + } + + function copyInto(target, source) { + if (target === source || isUndefinedOrNull(source)) { + return target; + } + + target = target || {}; + + // Copy into objects only + if (isObject(target)) { + // Make sure source exists + source = source || {}; + + if (isObject(source)) { + var i, newTarget, newSource; + for (i in source) { + if (source.hasOwnProperty(i)) { + newTarget = target[i]; + newSource = source[i]; + + if (newTarget && isObject(newSource)) { + // Deep copy + newTarget = copyInto(target[i], newSource); + } else { + newTarget = newSource; + } + + if (!isUndefined(newTarget)) { + target[i] = newTarget; + } + } + } + } else { + target = source; + } + } + + return target; + } + + function defineSetter(handlerName, signalName) { + Object.defineProperty(server, handlerName, { + set: function (f) { + if (handlers && typeof handlers[signalName] === 'function') { + try { + this[signalName].disconnect(handlers[signalName]); + } catch (e) {} + } + handlers[signalName] = f; + this[signalName].connect(handlers[signalName]); + } + }); + } + + defineSetter("onNewRequest", "newRequest"); + + server.listen = function (port, arg1, arg2) { + if (arguments.length === 2 && typeof arg1 === 'function') { + this.onNewRequest = arg1; + return this.listenOnPort(port, {}); + } + if (arguments.length === 3 && typeof arg2 === 'function') { + this.onNewRequest = arg2; + // arg1 == settings + return this.listenOnPort(port, arg1); + } + throw "Wrong use of WebServer#listen"; + }; + + // Copy options into server + if (opts) { + server = copyInto(server, opts); + } + + return server; +}; diff --git a/src/mongoose/ReadMe.txt b/src/mongoose/ReadMe.txt new file mode 100644 index 0000000..be1fd52 --- /dev/null +++ b/src/mongoose/ReadMe.txt @@ -0,0 +1,6 @@ +This project contains version 3.1 of the Mongoose web server project, as +found at http://code.google.com/p/mongoose. + +It contains the code for version 3.1 as of 26-May-2011 (revision 0ca751520abf). +It contains an additional change in pthread_cond_broadcast() [~line 865] to +improve stability when running a debug build. diff --git a/src/mongoose/mongoose.c b/src/mongoose/mongoose.c new file mode 100644 index 0000000..47685f4 --- /dev/null +++ b/src/mongoose/mongoose.c @@ -0,0 +1,4139 @@ +// Copyright (c) 2004-2010 Sergey Lyubka +// +// 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. + + +#if defined(__FreeBSD__) +#include <sys/socket.h> +#endif + +#if defined(_WIN32) +#define _WINSOCK_DEPRECATED_NO_WARNINGS +#else +#define _XOPEN_SOURCE 600 // For flockfile() on Linux +#define _LARGEFILE_SOURCE // Enable 64-bit file offsets +#define __STDC_FORMAT_MACROS // <inttypes.h> wants this for C++ +#endif + +#if defined(__SYMBIAN32__) +#define NO_SSL // SSL is not supported +#define NO_CGI // CGI is not supported +#define PATH_MAX FILENAME_MAX +#endif // __SYMBIAN32__ + +#ifndef _WIN32 +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +#include <signal.h> +#include <fcntl.h> +#endif // !_WIN32 + +#include <time.h> +#include <stdlib.h> +#include <stdarg.h> +#include <assert.h> +#include <string.h> +#include <ctype.h> +#include <limits.h> +#include <stddef.h> +#include <stdio.h> + +#if defined(_WIN32) && !defined(__SYMBIAN32__) // Windows specific +#define _WIN32_WINNT 0x0400 // To make it link in VS2005 +#include <windows.h> + +#ifndef PATH_MAX +#define PATH_MAX MAX_PATH +#endif + +#ifndef _WIN32 +#include <process.h> +#include <direct.h> +#else // _WIN32 +#include <winsock2.h> +#include <io.h> +#include <fcntl.h> + +typedef long off_t; + +#define strerror(x) _ultoa(x, (char *) _alloca(sizeof(x) *3 ), 10) +#endif // _WIN32 + +#define MAKEUQUAD(lo, hi) ((uint64_t)(((uint32_t)(lo)) | \ + ((uint64_t)((uint32_t)(hi))) << 32)) +#define RATE_DIFF 10000000 // 100 nsecs +#define EPOCH_DIFF MAKEUQUAD(0xd53e8000, 0x019db1de) +#define SYS2UNIX_TIME(lo, hi) \ + (time_t) ((MAKEUQUAD((lo), (hi)) - EPOCH_DIFF) / RATE_DIFF) + +// Visual Studio 6 does not know __func__ or __FUNCTION__ +// The rest of MS compilers use __FUNCTION__, not C99 __func__ +// Also use _strtoui64 on modern M$ compilers +#if defined(_MSC_VER) && _MSC_VER < 1300 +#define STRX(x) #x +#define STR(x) STRX(x) +#define __func__ "line " STR(__LINE__) +#define strtoull(x, y, z) strtoul(x, y, z) +#define strtoll(x, y, z) strtol(x, y, z) +#else +#define __func__ __FUNCTION__ +#define strtoull(x, y, z) _strtoui64(x, y, z) +#define strtoll(x, y, z) _strtoi64(x, y, z) +#endif // _MSC_VER + +#define ERRNO GetLastError() +#define NO_SOCKLEN_T +#define SSL_LIB "ssleay32.dll" +#define CRYPTO_LIB "libeay32.dll" +#define DIRSEP '\\' +#define IS_DIRSEP_CHAR(c) ((c) == '/' || (c) == '\\') +#define O_NONBLOCK 0 +#if !defined(EWOULDBLOCK) +#define EWOULDBLOCK WSAEWOULDBLOCK +#endif // !EWOULDBLOCK +#define _POSIX_ +#define INT64_FMT "I64d" + +#define WINCDECL __cdecl +#define SHUT_WR 1 +#define snprintf _snprintf +#define vsnprintf _vsnprintf +#define sleep(x) Sleep((x) * 1000) + +#define pipe(x) _pipe(x, BUFSIZ, _O_BINARY) +#define popen(x, y) _popen(x, y) +#define pclose(x) _pclose(x) +#define close(x) _close(x) +#define dlsym(x,y) GetProcAddress((HINSTANCE) (x), (y)) +#define RTLD_LAZY 0 +#define fseeko(x, y, z) fseek((x), (y), (z)) +#define fdopen(x, y) _fdopen((x), (y)) +#define write(x, y, z) _write((x), (y), (unsigned) z) +#define read(x, y, z) _read((x), (y), (unsigned) z) +#define flockfile(x) (void) 0 +#define funlockfile(x) (void) 0 + +#if !defined(fileno) +#define fileno(x) _fileno(x) +#endif // !fileno MINGW #defines fileno + +typedef HANDLE pthread_mutex_t; +typedef struct {HANDLE signal, broadcast;} pthread_cond_t; +typedef DWORD pthread_t; +#define pid_t HANDLE // MINGW typedefs pid_t to int. Using #define here. + +struct timespec { + long tv_nsec; + long tv_sec; +}; + +static int pthread_mutex_lock(pthread_mutex_t *); +static int pthread_mutex_unlock(pthread_mutex_t *); +static FILE *mg_fopen(const char *path, const char *mode); + +#if defined(HAVE_STDINT) +#include <stdint.h> +#else +typedef unsigned int uint32_t; +typedef unsigned short uint16_t; +typedef unsigned __int64 uint64_t; +typedef __int64 int64_t; +#define INT64_MAX 9223372036854775807 +#endif // HAVE_STDINT + +// POSIX dirent interface +struct dirent { + char d_name[PATH_MAX]; +}; + +typedef struct DIR { + HANDLE handle; + WIN32_FIND_DATAW info; + struct dirent result; +} DIR; + +#else // UNIX specific +#include <sys/wait.h> +#include <sys/socket.h> +#include <sys/select.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/time.h> +#include <stdint.h> +#include <inttypes.h> +#include <netdb.h> + +#include <pwd.h> +#include <unistd.h> +#include <dirent.h> +#if !defined(NO_SSL_DL) && !defined(NO_SSL) +#include <dlfcn.h> +#endif +#include <pthread.h> +#if defined(__MACH__) +#define SSL_LIB "libssl.dylib" +#define CRYPTO_LIB "libcrypto.dylib" +#else +#if !defined(SSL_LIB) +#define SSL_LIB "libssl.so" +#endif +#if !defined(CRYPTO_LIB) +#define CRYPTO_LIB "libcrypto.so" +#endif +#endif +#define DIRSEP '/' +#define IS_DIRSEP_CHAR(c) ((c) == '/') +#ifndef O_BINARY +#define O_BINARY 0 +#endif // O_BINARY +#define closesocket(a) close(a) +#define mg_fopen(x, y) fopen(x, y) +#define mg_mkdir(x, y) mkdir(x, y) +#define mg_remove(x) remove(x) +#define mg_rename(x, y) rename(x, y) +#define ERRNO errno +#define INVALID_SOCKET (-1) +#define INT64_FMT PRId64 +typedef int SOCKET; +#define WINCDECL + +#endif // End of Windows and UNIX specific includes + +#include "mongoose.h" + +#define MONGOOSE_VERSION "3.0" +#define PASSWORDS_FILE_NAME ".htpasswd" +#define CGI_ENVIRONMENT_SIZE 4096 +#define MAX_CGI_ENVIR_VARS 64 +#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0])) + +#if defined(DEBUG) +#define DEBUG_TRACE(x) do { \ + flockfile(stdout); \ + printf("*** %lu.%p.%s.%d: ", \ + (unsigned long) time(NULL), (void *) pthread_self(), \ + __func__, __LINE__); \ + printf x; \ + putchar('\n'); \ + fflush(stdout); \ + funlockfile(stdout); \ +} while (0) +#else +#define DEBUG_TRACE(x) +#endif // DEBUG + +// Darwin prior to 7.0 and Win32 do not have socklen_t +#ifdef NO_SOCKLEN_T +typedef int socklen_t; +#endif // NO_SOCKLEN_T + +typedef void * (*mg_thread_func_t)(void *); + +static const char *http_500_error = "Internal Server Error"; + +// Snatched from OpenSSL includes. I put the prototypes here to be independent +// from the OpenSSL source installation. Having this, mongoose + SSL can be +// built on any system with binary SSL libraries installed. +typedef struct ssl_st SSL; +typedef struct ssl_method_st SSL_METHOD; +typedef struct ssl_ctx_st SSL_CTX; + +#define SSL_ERROR_WANT_READ 2 +#define SSL_ERROR_WANT_WRITE 3 +#define SSL_FILETYPE_PEM 1 +#define CRYPTO_LOCK 1 + +#if defined(NO_SSL_DL) +extern void SSL_free(SSL *); +extern int SSL_accept(SSL *); +extern int SSL_connect(SSL *); +extern int SSL_read(SSL *, void *, int); +extern int SSL_write(SSL *, const void *, int); +extern int SSL_get_error(const SSL *, int); +extern int SSL_set_fd(SSL *, int); +extern SSL *SSL_new(SSL_CTX *); +extern SSL_CTX *SSL_CTX_new(SSL_METHOD *); +extern SSL_METHOD *SSLv23_server_method(void); +extern int SSL_library_init(void); +extern void SSL_load_error_strings(void); +extern int SSL_CTX_use_PrivateKey_file(SSL_CTX *, const char *, int); +extern int SSL_CTX_use_certificate_file(SSL_CTX *, const char *, int); +extern int SSL_CTX_use_certificate_chain_file(SSL_CTX *, const char *); +extern void SSL_CTX_set_default_passwd_cb(SSL_CTX *, mg_callback_t); +extern void SSL_CTX_free(SSL_CTX *); +extern unsigned long ERR_get_error(void); +extern char *ERR_error_string(unsigned long, char *); +extern int CRYPTO_num_locks(void); +extern void CRYPTO_set_locking_callback(void (*)(int, int, const char *, int)); +extern void CRYPTO_set_id_callback(unsigned long (*)(void)); +#else +// Dynamically loaded SSL functionality +struct ssl_func { + const char *name; // SSL function name + void (*ptr)(void); // Function pointer +}; + +#define SSL_free (* (void (*)(SSL *)) ssl_sw[0].ptr) +#define SSL_accept (* (int (*)(SSL *)) ssl_sw[1].ptr) +#define SSL_connect (* (int (*)(SSL *)) ssl_sw[2].ptr) +#define SSL_read (* (int (*)(SSL *, void *, int)) ssl_sw[3].ptr) +#define SSL_write (* (int (*)(SSL *, const void *,int)) ssl_sw[4].ptr) +#define SSL_get_error (* (int (*)(SSL *, int)) ssl_sw[5].ptr) +#define SSL_set_fd (* (int (*)(SSL *, SOCKET)) ssl_sw[6].ptr) +#define SSL_new (* (SSL * (*)(SSL_CTX *)) ssl_sw[7].ptr) +#define SSL_CTX_new (* (SSL_CTX * (*)(SSL_METHOD *)) ssl_sw[8].ptr) +#define SSLv23_server_method (* (SSL_METHOD * (*)(void)) ssl_sw[9].ptr) +#define SSL_library_init (* (int (*)(void)) ssl_sw[10].ptr) +#define SSL_CTX_use_PrivateKey_file (* (int (*)(SSL_CTX *, \ + const char *, int)) ssl_sw[11].ptr) +#define SSL_CTX_use_certificate_file (* (int (*)(SSL_CTX *, \ + const char *, int)) ssl_sw[12].ptr) +#define SSL_CTX_set_default_passwd_cb \ + (* (void (*)(SSL_CTX *, mg_callback_t)) ssl_sw[13].ptr) +#define SSL_CTX_free (* (void (*)(SSL_CTX *)) ssl_sw[14].ptr) +#define SSL_load_error_strings (* (void (*)(void)) ssl_sw[15].ptr) +#define SSL_CTX_use_certificate_chain_file \ + (* (int (*)(SSL_CTX *, const char *)) ssl_sw[16].ptr) + +#define CRYPTO_num_locks (* (int (*)(void)) crypto_sw[0].ptr) +#define CRYPTO_set_locking_callback \ + (* (void (*)(void (*)(int, int, const char *, int))) crypto_sw[1].ptr) +#define CRYPTO_set_id_callback \ + (* (void (*)(unsigned long (*)(void))) crypto_sw[2].ptr) +#define ERR_get_error (* (unsigned long (*)(void)) crypto_sw[3].ptr) +#define ERR_error_string (* (char * (*)(unsigned long,char *)) crypto_sw[4].ptr) + +// set_ssl_option() function updates this array. +// It loads SSL library dynamically and changes NULLs to the actual addresses +// of respective functions. The macros above (like SSL_connect()) are really +// just calling these functions indirectly via the pointer. +static struct ssl_func ssl_sw[] = { + {"SSL_free", NULL}, + {"SSL_accept", NULL}, + {"SSL_connect", NULL}, + {"SSL_read", NULL}, + {"SSL_write", NULL}, + {"SSL_get_error", NULL}, + {"SSL_set_fd", NULL}, + {"SSL_new", NULL}, + {"SSL_CTX_new", NULL}, + {"SSLv23_server_method", NULL}, + {"SSL_library_init", NULL}, + {"SSL_CTX_use_PrivateKey_file", NULL}, + {"SSL_CTX_use_certificate_file",NULL}, + {"SSL_CTX_set_default_passwd_cb",NULL}, + {"SSL_CTX_free", NULL}, + {"SSL_load_error_strings", NULL}, + {"SSL_CTX_use_certificate_chain_file", NULL}, + {NULL, NULL} +}; + +// Similar array as ssl_sw. These functions could be located in different lib. +static struct ssl_func crypto_sw[] = { + {"CRYPTO_num_locks", NULL}, + {"CRYPTO_set_locking_callback", NULL}, + {"CRYPTO_set_id_callback", NULL}, + {"ERR_get_error", NULL}, + {"ERR_error_string", NULL}, + {NULL, NULL} +}; +#endif // NO_SSL_DL + +static const char *month_names[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +// Unified socket address. For IPv6 support, add IPv6 address structure +// in the union u. +struct usa { + socklen_t len; + union { + struct sockaddr sa; + struct sockaddr_in sin; + } u; +}; + +// Describes a string (chunk of memory). +struct vec { + const char *ptr; + size_t len; +}; + +// Structure used by mg_stat() function. Uses 64 bit file length. +struct mgstat { + int is_directory; // Directory marker + int64_t size; // File size + time_t mtime; // Modification time +}; + +// Describes listening socket, or socket which was accept()-ed by the master +// thread and queued for future handling by the worker thread. +struct socket { + struct socket *next; // Linkage + SOCKET sock; // Listening socket + struct usa lsa; // Local socket address + struct usa rsa; // Remote socket address + int is_ssl; // Is socket SSL-ed + int is_proxy; +}; + +enum { + CGI_EXTENSIONS, CGI_ENVIRONMENT, PUT_DELETE_PASSWORDS_FILE, CGI_INTERPRETER, + PROTECT_URI, AUTHENTICATION_DOMAIN, SSI_EXTENSIONS, ACCESS_LOG_FILE, + SSL_CHAIN_FILE, ENABLE_DIRECTORY_LISTING, ERROR_LOG_FILE, + GLOBAL_PASSWORDS_FILE, INDEX_FILES, + ENABLE_KEEP_ALIVE, ACCESS_CONTROL_LIST, MAX_REQUEST_SIZE, + EXTRA_MIME_TYPES, LISTENING_PORTS, + DOCUMENT_ROOT, SSL_CERTIFICATE, NUM_THREADS, RUN_AS_USER, + NUM_OPTIONS +}; + +static const char *config_options[] = { + "C", "cgi_extensions", ".cgi,.pl,.php", + "E", "cgi_environment", NULL, + "G", "put_delete_passwords_file", NULL, + "I", "cgi_interpreter", NULL, + "P", "protect_uri", NULL, + "R", "authentication_domain", "mydomain.com", + "S", "ssi_extensions", ".shtml,.shtm", + "a", "access_log_file", NULL, + "c", "ssl_chain_file", NULL, + "d", "enable_directory_listing", "yes", + "e", "error_log_file", NULL, + "g", "global_passwords_file", NULL, + "i", "index_files", "index.html,index.htm,index.cgi", + "k", "enable_keep_alive", "no", + "l", "access_control_list", NULL, + "M", "max_request_size", "16384", + "m", "extra_mime_types", NULL, + "p", "listening_ports", "8080", + "r", "document_root", ".", + "s", "ssl_certificate", NULL, + "t", "num_threads", "10", + "u", "run_as_user", NULL, + NULL +}; +#define ENTRIES_PER_CONFIG_OPTION 3 + +struct mg_context { + volatile int stop_flag; // Should we stop event loop + SSL_CTX *ssl_ctx; // SSL context + char *config[NUM_OPTIONS]; // Mongoose configuration parameters + mg_callback_t user_callback; // User-defined callback function + void *user_data; // User-defined data + + struct socket *listening_sockets; + + volatile int num_threads; // Number of threads + pthread_mutex_t mutex; // Protects (max|num)_threads + pthread_cond_t cond; // Condvar for tracking workers terminations + + struct socket queue[20]; // Accepted sockets + volatile int sq_head; // Head of the socket queue + volatile int sq_tail; // Tail of the socket queue + pthread_cond_t sq_full; // Singaled when socket is produced + pthread_cond_t sq_empty; // Signaled when socket is consumed +}; + +struct mg_connection { + struct mg_connection *peer; // Remote target in proxy mode + struct mg_request_info request_info; + struct mg_context *ctx; + SSL *ssl; // SSL descriptor + struct socket client; // Connected client + time_t birth_time; // Time connection was accepted + int64_t num_bytes_sent; // Total bytes sent to client + int64_t content_len; // Content-Length header value + int64_t consumed_content; // How many bytes of content is already read + char *buf; // Buffer for received data + int buf_size; // Buffer size + int request_len; // Size of the request + headers in a buffer + int data_len; // Total size of data in a buffer +}; + +const char **mg_get_valid_option_names(void) { + return config_options; +} + +static void *call_user(struct mg_connection *conn, enum mg_event event) { + conn->request_info.user_data = conn->ctx->user_data; + return conn->ctx->user_callback == NULL ? NULL : + conn->ctx->user_callback(event, conn, &conn->request_info); +} + +static int get_option_index(const char *name) { + int i; + + for (i = 0; config_options[i] != NULL; i += ENTRIES_PER_CONFIG_OPTION) { + if (strcmp(config_options[i], name) == 0 || + strcmp(config_options[i + 1], name) == 0) { + return i / ENTRIES_PER_CONFIG_OPTION; + } + } + return -1; +} + +const char *mg_get_option(const struct mg_context *ctx, const char *name) { + int i; + if ((i = get_option_index(name)) == -1) { + return NULL; + } else if (ctx->config[i] == NULL) { + return ""; + } else { + return ctx->config[i]; + } +} + +// Print error message to the opened error log stream. +static void cry(struct mg_connection *conn, const char *fmt, ...) { + char buf[BUFSIZ]; + va_list ap; + FILE *fp; + time_t timestamp; + + va_start(ap, fmt); + (void) vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + // Do not lock when getting the callback value, here and below. + // I suppose this is fine, since function cannot disappear in the + // same way string option can. + conn->request_info.log_message = buf; + if (call_user(conn, MG_EVENT_LOG) == NULL) { + fp = conn->ctx->config[ERROR_LOG_FILE] == NULL ? NULL : + mg_fopen(conn->ctx->config[ERROR_LOG_FILE], "a+"); + + if (fp != NULL) { + flockfile(fp); + timestamp = time(NULL); + + (void) fprintf(fp, + "[%010lu] [error] [client %s] ", + (unsigned long) timestamp, + inet_ntoa(conn->client.rsa.u.sin.sin_addr)); + + if (conn->request_info.request_method != NULL) { + (void) fprintf(fp, "%s %s: ", + conn->request_info.request_method, + conn->request_info.uri); + } + + (void) fprintf(fp, "%s", buf); + fputc('\n', fp); + funlockfile(fp); + if (fp != stderr) { + fclose(fp); + } + } + } + conn->request_info.log_message = NULL; +} + +// Return OpenSSL error message +static const char *ssl_error(void) { + unsigned long err; + err = ERR_get_error(); + return err == 0 ? "" : ERR_error_string(err, NULL); +} + +// Return fake connection structure. Used for logging, if connection +// is not applicable at the moment of logging. +static struct mg_connection *fc(struct mg_context *ctx) { + static struct mg_connection fake_connection; + fake_connection.ctx = ctx; + return &fake_connection; +} + +const char *mg_version(void) { + return MONGOOSE_VERSION; +} + +static void mg_strlcpy(register char *dst, register const char *src, size_t n) { + for (; *src != '\0' && n > 1; n--) { + *dst++ = *src++; + } + *dst = '\0'; +} + +static int lowercase(const char *s) { + return tolower(* (const unsigned char *) s); +} + +static int mg_strncasecmp(const char *s1, const char *s2, size_t len) { + int diff = 0; + + if (len > 0) + do { + diff = lowercase(s1++) - lowercase(s2++); + } while (diff == 0 && s1[-1] != '\0' && --len > 0); + + return diff; +} + +static int mg_strcasecmp(const char *s1, const char *s2) { + int diff; + + do { + diff = lowercase(s1++) - lowercase(s2++); + } while (diff == 0 && s1[-1] != '\0'); + + return diff; +} + +static char * mg_strndup(const char *ptr, size_t len) { + char *p; + + if ((p = (char *) malloc(len + 1)) != NULL) { + mg_strlcpy(p, ptr, len + 1); + } + + return p; +} + +static char * mg_strdup(const char *str) { + return mg_strndup(str, strlen(str)); +} + +// Like snprintf(), but never returns negative value, or the value +// that is larger than a supplied buffer. +// Thanks to Adam Zeldis to pointing snprintf()-caused vulnerability +// in his audit report. +static int mg_vsnprintf(struct mg_connection *conn, char *buf, size_t buflen, + const char *fmt, va_list ap) { + int n; + + if (buflen == 0) + return 0; + + n = vsnprintf(buf, buflen, fmt, ap); + + if (n < 0) { + cry(conn, "vsnprintf error"); + n = 0; + } else if (n >= (int) buflen) { + cry(conn, "truncating vsnprintf buffer: [%.*s]", + n > 200 ? 200 : n, buf); + n = (int) buflen - 1; + } + buf[n] = '\0'; + + return n; +} + +static int mg_snprintf(struct mg_connection *conn, char *buf, size_t buflen, + const char *fmt, ...) { + va_list ap; + int n; + + va_start(ap, fmt); + n = mg_vsnprintf(conn, buf, buflen, fmt, ap); + va_end(ap); + + return n; +} + +// Skip the characters until one of the delimiters characters found. +// 0-terminate resulting word. Skip the delimiter and following whitespaces if any. +// Advance pointer to buffer to the next word. Return found 0-terminated word. +// Delimiters can be quoted with quotechar. +static char *skip_quoted(char **buf, const char *delimiters, const char *whitespace, char quotechar) { + char *p, *begin_word, *end_word, *end_whitespace; + + begin_word = *buf; + end_word = begin_word + strcspn(begin_word, delimiters); + + /* Check for quotechar */ + if (end_word > begin_word) { + p = end_word - 1; + while (*p == quotechar) { + /* If there is anything beyond end_word, copy it */ + if (*end_word == '\0') { + *p = '\0'; + break; + } else { + size_t end_off = strcspn(end_word + 1, delimiters); + memmove (p, end_word, end_off + 1); + p += end_off; /* p must correspond to end_word - 1 */ + end_word += end_off + 1; + } + } + for (p++; p < end_word; p++) { + *p = '\0'; + } + } + + if (*end_word == '\0') { + *buf = end_word; + } else { + end_whitespace = end_word + 1 + strspn(end_word + 1, whitespace); + + for (p = end_word; p < end_whitespace; p++) { + *p = '\0'; + } + + *buf = end_whitespace; + } + + return begin_word; +} + +// Simplified version of skip_quoted without quote char +// and whitespace == delimiters +static char *skip(char **buf, const char *delimiters) { + return skip_quoted(buf, delimiters, delimiters, 0); +} + + +// Return HTTP header value, or NULL if not found. +static const char *get_header(const struct mg_request_info *ri, + const char *name) { + int i; + + for (i = 0; i < ri->num_headers; i++) + if (!mg_strcasecmp(name, ri->http_headers[i].name)) + return ri->http_headers[i].value; + + return NULL; +} + +const char *mg_get_header(const struct mg_connection *conn, const char *name) { + return get_header(&conn->request_info, name); +} + +// A helper function for traversing comma separated list of values. +// It returns a list pointer shifted to the next value, of NULL if the end +// of the list found. +// Value is stored in val vector. If value has form "x=y", then eq_val +// vector is initialized to point to the "y" part, and val vector length +// is adjusted to point only to "x". +static const char *next_option(const char *list, struct vec *val, + struct vec *eq_val) { + if (list == NULL || *list == '\0') { + /* End of the list */ + list = NULL; + } else { + val->ptr = list; + if ((list = strchr(val->ptr, ',')) != NULL) { + /* Comma found. Store length and shift the list ptr */ + val->len = list - val->ptr; + list++; + } else { + /* This value is the last one */ + list = val->ptr + strlen(val->ptr); + val->len = list - val->ptr; + } + + if (eq_val != NULL) { + /* + * Value has form "x=y", adjust pointers and lengths + * so that val points to "x", and eq_val points to "y". + */ + eq_val->len = 0; + eq_val->ptr = (const char *) memchr(val->ptr, '=', val->len); + if (eq_val->ptr != NULL) { + eq_val->ptr++; /* Skip over '=' character */ + eq_val->len = val->ptr + val->len - eq_val->ptr; + val->len = (eq_val->ptr - val->ptr) - 1; + } + } + } + + return list; +} + +#if !defined(NO_CGI) +static int match_extension(const char *path, const char *ext_list) { + struct vec ext_vec; + size_t path_len; + + path_len = strlen(path); + + while ((ext_list = next_option(ext_list, &ext_vec, NULL)) != NULL) + if (ext_vec.len < path_len && + mg_strncasecmp(path + path_len - ext_vec.len, + ext_vec.ptr, ext_vec.len) == 0) + return 1; + + return 0; +} +#endif // !NO_CGI + +// HTTP 1.1 assumes keep alive if "Connection:" header is not set +// This function must tolerate situations when connection info is not +// set up, for example if request parsing failed. +static int should_keep_alive(const struct mg_connection *conn) { + const char *http_version = conn->request_info.http_version; + const char *header = mg_get_header(conn, "Connection"); + return (header == NULL && http_version && !strcmp(http_version, "1.1")) || + (header != NULL && !mg_strcasecmp(header, "keep-alive")); +} + +static const char *suggest_connection_header(const struct mg_connection *conn) { + return should_keep_alive(conn) ? "keep-alive" : "close"; +} + +static void send_http_error(struct mg_connection *conn, int status, + const char *reason, const char *fmt, ...) { + char buf[BUFSIZ]; + va_list ap; + int len; + + conn->request_info.status_code = status; + + if (call_user(conn, MG_HTTP_ERROR) == NULL) { + buf[0] = '\0'; + len = 0; + + /* Errors 1xx, 204 and 304 MUST NOT send a body */ + if (status > 199 && status != 204 && status != 304) { + len = mg_snprintf(conn, buf, sizeof(buf), "Error %d: %s", status, reason); + cry(conn, "%s", buf); + buf[len++] = '\n'; + + va_start(ap, fmt); + len += mg_vsnprintf(conn, buf + len, sizeof(buf) - len, fmt, ap); + va_end(ap); + } + DEBUG_TRACE(("[%s]", buf)); + + mg_printf(conn, "HTTP/1.1 %d %s\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: %d\r\n" + "Connection: %s\r\n\r\n", status, reason, len, + suggest_connection_header(conn)); + conn->num_bytes_sent += mg_printf(conn, "%s", buf); + } +} + +#if defined(_WIN32) && !defined(__SYMBIAN32__) +static int pthread_mutex_init(pthread_mutex_t *mutex, void *unused) { + unused = NULL; + *mutex = CreateMutex(NULL, FALSE, NULL); + return *mutex == NULL ? -1 : 0; +} + +static int pthread_mutex_destroy(pthread_mutex_t *mutex) { + return CloseHandle(*mutex) == 0 ? -1 : 0; +} + +static int pthread_mutex_lock(pthread_mutex_t *mutex) { + return WaitForSingleObject(*mutex, INFINITE) == WAIT_OBJECT_0? 0 : -1; +} + +static int pthread_mutex_unlock(pthread_mutex_t *mutex) { + return ReleaseMutex(*mutex) == 0 ? -1 : 0; +} + +static int pthread_cond_init(pthread_cond_t *cv, const void *unused) { + unused = NULL; + cv->signal = CreateEvent(NULL, FALSE, FALSE, NULL); + cv->broadcast = CreateEvent(NULL, TRUE, FALSE, NULL); + return cv->signal != NULL && cv->broadcast != NULL ? 0 : -1; +} + +static int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex) { + HANDLE handles[] = {cv->signal, cv->broadcast}; + ReleaseMutex(*mutex); + WaitForMultipleObjects(2, handles, FALSE, INFINITE); + return WaitForSingleObject(*mutex, INFINITE) == WAIT_OBJECT_0? 0 : -1; +} + +static int pthread_cond_signal(pthread_cond_t *cv) { + return SetEvent(cv->signal) == 0 ? -1 : 0; +} + +static int pthread_cond_broadcast(pthread_cond_t *cv) { + // Implementation with PulseEvent() has race condition, see + // http://www.cs.wustl.edu/~schmidt/win32-cv-1.html + return PulseEvent(cv->broadcast) == 0 ? -1 : 0; +} + +static int pthread_cond_destroy(pthread_cond_t *cv) { + return CloseHandle(cv->signal) && CloseHandle(cv->broadcast) ? 0 : -1; +} + +static pthread_t pthread_self(void) { + return GetCurrentThreadId(); +} + +// For Windows, change all slashes to backslashes in path names. +static void change_slashes_to_backslashes(char *path) { + int i; + + for (i = 0; path[i] != '\0'; i++) { + if (path[i] == '/') + path[i] = '\\'; + // i > 0 check is to preserve UNC paths, like \\server\file.txt + if (path[i] == '\\' && i > 0) + while (path[i + 1] == '\\' || path[i + 1] == '/') + (void) memmove(path + i + 1, + path + i + 2, strlen(path + i + 1)); + } +} + +// Encode 'path' which is assumed UTF-8 string, into UNICODE string. +// wbuf and wbuf_len is a target buffer and its length. +static void to_unicode(const char *path, wchar_t *wbuf, size_t wbuf_len) { + char buf[PATH_MAX], *p; + + mg_strlcpy(buf, path, sizeof(buf)); + change_slashes_to_backslashes(buf); + + // Point p to the end of the file name + p = buf + strlen(buf) - 1; + + // Trim trailing backslash character + while (p > buf && *p == '\\' && p[-1] != ':') { + *p-- = '\0'; + } + + // Protect from CGI code disclosure. + // This is very nasty hole. Windows happily opens files with + // some garbage in the end of file name. So fopen("a.cgi ", "r") + // actually opens "a.cgi", and does not return an error! + if (*p == 0x20 || // No space at the end + (*p == 0x2e && p > buf) || // No '.' but allow '.' as full path + *p == 0x2b || // No '+' + (*p & ~0x7f)) { // And generally no non-ascii chars + (void) fprintf(stderr, "Rejecting suspicious path: [%s]", buf); + buf[0] = '\0'; + } + + (void) MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, (int) wbuf_len); +} + +#if defined(_WIN32_WCE) +static time_t time(time_t *ptime) { + time_t t; + SYSTEMTIME st; + FILETIME ft; + + GetSystemTime(&st); + SystemTimeToFileTime(&st, &ft); + t = SYS2UNIX_TIME(ft.dwLowDateTime, ft.dwHighDateTime); + + if (ptime != NULL) { + *ptime = t; + } + + return t; +} + +static time_t mktime(struct tm *ptm) { + SYSTEMTIME st; + FILETIME ft, lft; + + st.wYear = ptm->tm_year + 1900; + st.wMonth = ptm->tm_mon + 1; + st.wDay = ptm->tm_mday; + st.wHour = ptm->tm_hour; + st.wMinute = ptm->tm_min; + st.wSecond = ptm->tm_sec; + st.wMilliseconds = 0; + + SystemTimeToFileTime(&st, &ft); + LocalFileTimeToFileTime(&ft, &lft); + return (time_t) ((MAKEUQUAD(lft.dwLowDateTime, lft.dwHighDateTime) - + EPOCH_DIFF) / RATE_DIFF); +} + +static struct tm *localtime(const time_t *ptime, struct tm *ptm) { + int64_t t = ((int64_t) *ptime) * RATE_DIFF + EPOCH_DIFF; + FILETIME ft, lft; + SYSTEMTIME st; + TIME_ZONE_INFORMATION tzinfo; + + if (ptm == NULL) { + return NULL; + } + + * (int64_t *) &ft = t; + FileTimeToLocalFileTime(&ft, &lft); + FileTimeToSystemTime(&lft, &st); + ptm->tm_year = st.wYear - 1900; + ptm->tm_mon = st.wMonth - 1; + ptm->tm_wday = st.wDayOfWeek; + ptm->tm_mday = st.wDay; + ptm->tm_hour = st.wHour; + ptm->tm_min = st.wMinute; + ptm->tm_sec = st.wSecond; + ptm->tm_yday = 0; // hope nobody uses this + ptm->tm_isdst = + GetTimeZoneInformation(&tzinfo) == TIME_ZONE_ID_DAYLIGHT ? 1 : 0; + + return ptm; +} + +static size_t strftime(char *dst, size_t dst_size, const char *fmt, + const struct tm *tm) { + (void) snprintf(dst, dst_size, "implement strftime() for WinCE"); + return 0; +} +#endif + +static int mg_rename(const char* oldname, const char* newname) { + wchar_t woldbuf[PATH_MAX]; + wchar_t wnewbuf[PATH_MAX]; + + to_unicode(oldname, woldbuf, ARRAY_SIZE(woldbuf)); + to_unicode(newname, wnewbuf, ARRAY_SIZE(wnewbuf)); + + return MoveFileW(woldbuf, wnewbuf) ? 0 : -1; +} + + +static FILE *mg_fopen(const char *path, const char *mode) { + wchar_t wbuf[PATH_MAX], wmode[20]; + + to_unicode(path, wbuf, ARRAY_SIZE(wbuf)); + MultiByteToWideChar(CP_UTF8, 0, mode, -1, wmode, ARRAY_SIZE(wmode)); + + return _wfopen(wbuf, wmode); +} + +static int mg_stat(const char *path, struct mgstat *stp) { + int ok = -1; // Error + wchar_t wbuf[PATH_MAX]; + WIN32_FILE_ATTRIBUTE_DATA info; + + to_unicode(path, wbuf, ARRAY_SIZE(wbuf)); + + if (GetFileAttributesExW(wbuf, GetFileExInfoStandard, &info) != 0) { + stp->size = MAKEUQUAD(info.nFileSizeLow, info.nFileSizeHigh); + stp->mtime = SYS2UNIX_TIME(info.ftLastWriteTime.dwLowDateTime, + info.ftLastWriteTime.dwHighDateTime); + stp->is_directory = + info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; + ok = 0; // Success + } + + return ok; +} + +static int mg_remove(const char *path) { + wchar_t wbuf[PATH_MAX]; + to_unicode(path, wbuf, ARRAY_SIZE(wbuf)); + return DeleteFileW(wbuf) ? 0 : -1; +} + +static int mg_mkdir(const char *path, int mode) { + char buf[PATH_MAX]; + wchar_t wbuf[PATH_MAX]; + + mode = 0; // Unused + mg_strlcpy(buf, path, sizeof(buf)); + change_slashes_to_backslashes(buf); + + (void) MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf)); + + return CreateDirectoryW(wbuf, NULL) ? 0 : -1; +} + +// Implementation of POSIX opendir/closedir/readdir for Windows. +static DIR * opendir(const char *name) { + DIR *dir = NULL; + wchar_t wpath[PATH_MAX]; + DWORD attrs; + + if (name == NULL) { + SetLastError(ERROR_BAD_ARGUMENTS); + } else if ((dir = (DIR *) malloc(sizeof(*dir))) == NULL) { + SetLastError(ERROR_NOT_ENOUGH_MEMORY); + } else { + to_unicode(name, wpath, ARRAY_SIZE(wpath)); + attrs = GetFileAttributesW(wpath); + if (attrs != 0xFFFFFFFF && + ((attrs & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY)) { + (void) wcscat(wpath, L"\\*"); + dir->handle = FindFirstFileW(wpath, &dir->info); + dir->result.d_name[0] = '\0'; + } else { + free(dir); + dir = NULL; + } + } + + return dir; +} + +static int closedir(DIR *dir) { + int result = 0; + + if (dir != NULL) { + if (dir->handle != INVALID_HANDLE_VALUE) + result = FindClose(dir->handle) ? 0 : -1; + + free(dir); + } else { + result = -1; + SetLastError(ERROR_BAD_ARGUMENTS); + } + + return result; +} + +struct dirent * readdir(DIR *dir) { + struct dirent *result = 0; + + if (dir) { + if (dir->handle != INVALID_HANDLE_VALUE) { + result = &dir->result; + (void) WideCharToMultiByte(CP_UTF8, 0, + dir->info.cFileName, -1, result->d_name, + sizeof(result->d_name), NULL, NULL); + + if (!FindNextFileW(dir->handle, &dir->info)) { + (void) FindClose(dir->handle); + dir->handle = INVALID_HANDLE_VALUE; + } + + } else { + SetLastError(ERROR_FILE_NOT_FOUND); + } + } else { + SetLastError(ERROR_BAD_ARGUMENTS); + } + + return result; +} + +#define set_close_on_exec(fd) // No FD_CLOEXEC on Windows + +static int start_thread(struct mg_context *ctx, mg_thread_func_t func, + void *param) { + HANDLE hThread; + ctx = NULL; // Unused + + hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) func, param, 0, + NULL); + if (hThread != NULL) { + (void) CloseHandle(hThread); + } + + return hThread == NULL ? -1 : 0; +} + +static HANDLE dlopen(const char *dll_name, int flags) { + wchar_t wbuf[PATH_MAX]; + flags = 0; // Unused + to_unicode(dll_name, wbuf, ARRAY_SIZE(wbuf)); + return LoadLibraryW(wbuf); +} + +#if !defined(NO_CGI) +#define SIGKILL 0 +static int kill(pid_t pid, int sig_num) { + (void) TerminateProcess(pid, sig_num); + (void) CloseHandle(pid); + return 0; +} + +static pid_t spawn_process(struct mg_connection *conn, const char *prog, + char *envblk, char *envp[], int fd_stdin, + int fd_stdout, const char *dir) { + HANDLE me; + char *p, *interp, cmdline[PATH_MAX], buf[PATH_MAX]; + FILE *fp; + STARTUPINFOA si; + PROCESS_INFORMATION pi; + + envp = NULL; // Unused + + (void) memset(&si, 0, sizeof(si)); + (void) memset(&pi, 0, sizeof(pi)); + + // TODO(lsm): redirect CGI errors to the error log file + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + + me = GetCurrentProcess(); + (void) DuplicateHandle(me, (HANDLE) _get_osfhandle(fd_stdin), me, + &si.hStdInput, 0, TRUE, DUPLICATE_SAME_ACCESS); + (void) DuplicateHandle(me, (HANDLE) _get_osfhandle(fd_stdout), me, + &si.hStdOutput, 0, TRUE, DUPLICATE_SAME_ACCESS); + + // If CGI file is a script, try to read the interpreter line + interp = conn->ctx->config[CGI_INTERPRETER]; + if (interp == NULL) { + buf[2] = '\0'; + if ((fp = fopen(cmdline, "r")) != NULL) { + (void) fgets(buf, sizeof(buf), fp); + if (buf[0] != '#' || buf[1] != '!') { + // First line does not start with "#!". Do not set interpreter. + buf[2] = '\0'; + } else { + // Trim whitespaces in interpreter name + for (p = &buf[strlen(buf) - 1]; p > buf && isspace(*p); p--) { + *p = '\0'; + } + } + (void) fclose(fp); + } + interp = buf + 2; + } + + (void) mg_snprintf(conn, cmdline, sizeof(cmdline), "%s%s%s%c%s", + interp, interp[0] == '\0' ? "" : " ", dir, DIRSEP, prog); + + DEBUG_TRACE(("Running [%s]", cmdline)); + if (CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, + CREATE_NEW_PROCESS_GROUP, envblk, dir, &si, &pi) == 0) { + cry(conn, "%s: CreateProcess(%s): %d", + __func__, cmdline, ERRNO); + pi.hProcess = (pid_t) -1; + } else { + (void) close(fd_stdin); + (void) close(fd_stdout); + } + + (void) CloseHandle(si.hStdOutput); + (void) CloseHandle(si.hStdInput); + (void) CloseHandle(pi.hThread); + + return (pid_t) pi.hProcess; +} +#endif /* !NO_CGI */ + +static int set_non_blocking_mode(SOCKET sock) { + unsigned long on = 1; + return ioctlsocket(sock, FIONBIO, &on); +} + +#else +static int mg_stat(const char *path, struct mgstat *stp) { + struct stat st; + int ok; + + if (stat(path, &st) == 0) { + ok = 0; + stp->size = st.st_size; + stp->mtime = st.st_mtime; + stp->is_directory = S_ISDIR(st.st_mode); + } else { + ok = -1; + } + + return ok; +} + +static void set_close_on_exec(int fd) { + (void) fcntl(fd, F_SETFD, FD_CLOEXEC); +} + +static int start_thread(struct mg_context *ctx, mg_thread_func_t func, + void *param) { + pthread_t thread_id; + pthread_attr_t attr; + int retval; + + (void) pthread_attr_init(&attr); + (void) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + // TODO(lsm): figure out why mongoose dies on Linux if next line is enabled + // (void) pthread_attr_setstacksize(&attr, sizeof(struct mg_connection) * 5); + + if ((retval = pthread_create(&thread_id, &attr, func, param)) != 0) { + cry(fc(ctx), "%s: %s", __func__, strerror(retval)); + } + + return retval; +} + +#ifndef NO_CGI +static pid_t spawn_process(struct mg_connection *conn, const char *prog, + char *envblk, char *envp[], int fd_stdin, + int fd_stdout, const char *dir) { + pid_t pid; + const char *interp; + + envblk = NULL; // Unused + + if ((pid = fork()) == -1) { + // Parent + send_http_error(conn, 500, http_500_error, "fork(): %s", strerror(ERRNO)); + } else if (pid == 0) { + // Child + if (chdir(dir) != 0) { + cry(conn, "%s: chdir(%s): %s", __func__, dir, strerror(ERRNO)); + } else if (dup2(fd_stdin, 0) == -1) { + cry(conn, "%s: dup2(%d, 0): %s", __func__, fd_stdin, strerror(ERRNO)); + } else if (dup2(fd_stdout, 1) == -1) { + cry(conn, "%s: dup2(%d, 1): %s", __func__, fd_stdout, strerror(ERRNO)); + } else { + (void) dup2(fd_stdout, 2); + (void) close(fd_stdin); + (void) close(fd_stdout); + + // Execute CGI program. No need to lock: new process + interp = conn->ctx->config[CGI_INTERPRETER]; + if (interp == NULL) { + (void) execle(prog, prog, NULL, envp); + cry(conn, "%s: execle(%s): %s", __func__, prog, strerror(ERRNO)); + } else { + (void) execle(interp, interp, prog, NULL, envp); + cry(conn, "%s: execle(%s %s): %s", __func__, interp, prog, + strerror(ERRNO)); + } + } + exit(EXIT_FAILURE); + } else { + // Parent. Close stdio descriptors + (void) close(fd_stdin); + (void) close(fd_stdout); + } + + return pid; +} +#endif // !NO_CGI + +static int set_non_blocking_mode(SOCKET sock) { + int flags; + + flags = fcntl(sock, F_GETFL, 0); + (void) fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + return 0; +} +#endif // _WIN32 + +// Write data to the IO channel - opened file descriptor, socket or SSL +// descriptor. Return number of bytes written. +static int64_t push(FILE *fp, SOCKET sock, SSL *ssl, const char *buf, + int64_t len) { + int64_t sent; + int n, k; + + sent = 0; + while (sent < len) { + + /* How many bytes we send in this iteration */ + k = len - sent > INT_MAX ? INT_MAX : (int) (len - sent); + + if (ssl != NULL) { + n = SSL_write(ssl, buf + sent, k); + } else if (fp != NULL) { + n = fwrite(buf + sent, 1, (size_t)k, fp); + if (ferror(fp)) + n = -1; + } else { + n = send(sock, buf + sent, (size_t)k, 0); + } + + if (n < 0) + break; + + sent += n; + } + + return sent; +} + +// Read from IO channel - opened file descriptor, socket, or SSL descriptor. +// Return number of bytes read. +static int pull(FILE *fp, SOCKET sock, SSL *ssl, char *buf, int len) { + int nread; + + if (ssl != NULL) { + nread = SSL_read(ssl, buf, len); + } else if (fp != NULL) { + // Use read() instead of fread(), because if we're reading from the CGI + // pipe, fread() may block until IO buffer is filled up. We cannot afford + // to block and must pass all read bytes immediately to the client. + nread = read(fileno(fp), buf, (size_t) len); + if (ferror(fp)) + nread = -1; + } else { + nread = recv(sock, buf, (size_t) len, 0); + } + + return nread; +} + +int mg_read(struct mg_connection *conn, void *buf, size_t len) { + int n, buffered_len, nread; + const char *buffered; + + assert((conn->content_len == -1 && conn->consumed_content == 0) || + conn->consumed_content <= conn->content_len); + DEBUG_TRACE(("%p %zu %lld %lld", buf, len, + conn->content_len, conn->consumed_content)); + nread = 0; + if (conn->consumed_content < conn->content_len) { + + // Adjust number of bytes to read. + int64_t to_read = conn->content_len - conn->consumed_content; + if (to_read < (int64_t) len) { + len = (int) to_read; + } + + // How many bytes of data we have buffered in the request buffer? + buffered = conn->buf + conn->request_len + conn->consumed_content; + buffered_len = conn->data_len - conn->request_len; + assert(buffered_len >= 0); + + // Return buffered data back if we haven't done that yet. + if (conn->consumed_content < (int64_t) buffered_len) { + buffered_len -= (int) conn->consumed_content; + if (len < (size_t) buffered_len) { + buffered_len = len; + } + memcpy(buf, buffered, (size_t)buffered_len); + len -= buffered_len; + buf = (char *) buf + buffered_len; + conn->consumed_content += buffered_len; + nread = buffered_len; + } + + // We have returned all buffered data. Read new data from the remote socket. + while (len > 0) { + n = pull(NULL, conn->client.sock, conn->ssl, (char *) buf, (int) len); + if (n <= 0) { + break; + } + buf = (char *) buf + n; + conn->consumed_content += n; + nread += n; + len -= n; + } + } + return nread; +} + +int mg_write(struct mg_connection *conn, const void *buf, size_t len) { + return (int) push(NULL, conn->client.sock, conn->ssl, + (const char *) buf, (int64_t) len); +} + +int mg_printf(struct mg_connection *conn, const char *fmt, ...) { + char buf[BUFSIZ]; + int len; + va_list ap; + + va_start(ap, fmt); + len = mg_vsnprintf(conn, buf, sizeof(buf), fmt, ap); + va_end(ap); + + return mg_write(conn, buf, (size_t)len); +} + +// URL-decode input buffer into destination buffer. +// 0-terminate the destination buffer. Return the length of decoded data. +// form-url-encoded data differs from URI encoding in a way that it +// uses '+' as character for space, see RFC 1866 section 8.2.1 +// http://ftp.ics.uci.edu/pub/ietf/html/rfc1866.txt +static size_t url_decode(const char *src, size_t src_len, char *dst, + size_t dst_len, int is_form_url_encoded) { + size_t i, j; + int a, b; +#define HEXTOI(x) (isdigit(x) ? x - '0' : x - 'W') + + for (i = j = 0; i < src_len && j < dst_len - 1; i++, j++) { + if (src[i] == '%' && + isxdigit(* (const unsigned char *) (src + i + 1)) && + isxdigit(* (const unsigned char *) (src + i + 2))) { + a = tolower(* (const unsigned char *) (src + i + 1)); + b = tolower(* (const unsigned char *) (src + i + 2)); + dst[j] = (char) ((HEXTOI(a) << 4) | HEXTOI(b)); + i += 2; + } else if (is_form_url_encoded && src[i] == '+') { + dst[j] = ' '; + } else { + dst[j] = src[i]; + } + } + + dst[j] = '\0'; /* Null-terminate the destination */ + + return j; +} + +// Scan given buffer and fetch the value of the given variable. +// It can be specified in query string, or in the POST data. +// Return NULL if the variable not found, or allocated 0-terminated value. +// It is caller's responsibility to free the returned value. +int mg_get_var(const char *buf, size_t buf_len, const char *name, + char *dst, size_t dst_len) { + const char *p, *e, *s; + size_t name_len, len; + + name_len = strlen(name); + e = buf + buf_len; + len = -1; + dst[0] = '\0'; + + // buf is "var1=val1&var2=val2...". Find variable first + for (p = buf; p != NULL && p + name_len < e; p++) { + if ((p == buf || p[-1] == '&') && p[name_len] == '=' && + !mg_strncasecmp(name, p, name_len)) { + + // Point p to variable value + p += name_len + 1; + + // Point s to the end of the value + s = (const char *) memchr(p, '&', (size_t)(e - p)); + if (s == NULL) { + s = e; + } + assert(s >= p); + + // Decode variable into destination buffer + if ((size_t) (s - p) < dst_len) { + len = url_decode(p, (size_t)(s - p), dst, dst_len, 1); + } + break; + } + } + + return len; +} + +int mg_get_cookie(const struct mg_connection *conn, const char *cookie_name, + char *dst, size_t dst_size) { + const char *s, *p, *end; + int name_len, len = -1; + + dst[0] = '\0'; + if ((s = mg_get_header(conn, "Cookie")) == NULL) { + return 0; + } + + name_len = strlen(cookie_name); + end = s + strlen(s); + + for (; (s = strstr(s, cookie_name)) != NULL; s += name_len) + if (s[name_len] == '=') { + s += name_len + 1; + if ((p = strchr(s, ' ')) == NULL) + p = end; + if (p[-1] == ';') + p--; + if (*s == '"' && p[-1] == '"' && p > s + 1) { + s++; + p--; + } + if ((size_t) (p - s) < dst_size) { + len = (p - s) + 1; + mg_strlcpy(dst, s, (size_t)len); + } + break; + } + + return len; +} + +// Mongoose allows to specify multiple directories to serve, +// like /var/www,/~bob=/home/bob. That means that root directory depends on URI. +// This function returns root dir for given URI. +static int get_document_root(const struct mg_connection *conn, + struct vec *document_root) { + const char *root, *uri; + int len_of_matched_uri; + struct vec uri_vec, path_vec; + + uri = conn->request_info.uri; + len_of_matched_uri = 0; + root = next_option(conn->ctx->config[DOCUMENT_ROOT], document_root, NULL); + + while ((root = next_option(root, &uri_vec, &path_vec)) != NULL) { + if (memcmp(uri, uri_vec.ptr, uri_vec.len) == 0) { + *document_root = path_vec; + len_of_matched_uri = uri_vec.len; + break; + } + } + + return len_of_matched_uri; +} + +static void convert_uri_to_file_name(struct mg_connection *conn, + const char *uri, char *buf, + size_t buf_len) { + struct vec vec; + int match_len; + + match_len = get_document_root(conn, &vec); + mg_snprintf(conn, buf, buf_len, "%.*s%s", vec.len, vec.ptr, uri + match_len); + +#if defined(_WIN32) && !defined(__SYMBIAN32__) + change_slashes_to_backslashes(buf); +#endif /* _WIN32 */ + + DEBUG_TRACE(("[%s] -> [%s], [%.*s]", uri, buf, (int) vec.len, vec.ptr)); +} + +static int sslize(struct mg_connection *conn, int (*func)(SSL *)) { + return (conn->ssl = SSL_new(conn->ctx->ssl_ctx)) != NULL && + SSL_set_fd(conn->ssl, conn->client.sock) == 1 && + func(conn->ssl) == 1; +} + +static struct mg_connection *mg_connect(struct mg_connection *conn, + const char *host, int port, int use_ssl) { + struct mg_connection *newconn = NULL; + struct sockaddr_in sin; + struct hostent *he; + int sock; + + if (conn->ctx->ssl_ctx == NULL && use_ssl) { + cry(conn, "%s: SSL is not initialized", __func__); + } else if ((he = gethostbyname(host)) == NULL) { + cry(conn, "%s: gethostbyname(%s): %s", __func__, host, strerror(ERRNO)); + } else if ((sock = socket(PF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) { + cry(conn, "%s: socket: %s", __func__, strerror(ERRNO)); + } else { + sin.sin_family = AF_INET; + sin.sin_port = htons((uint16_t) port); + sin.sin_addr = * (struct in_addr *) he->h_addr_list[0]; + if (connect(sock, (struct sockaddr *) &sin, sizeof(sin)) != 0) { + cry(conn, "%s: connect(%s:%d): %s", __func__, host, port, + strerror(ERRNO)); + closesocket(sock); + } else if ((newconn = (struct mg_connection *) + calloc(1, sizeof(*newconn))) == NULL) { + cry(conn, "%s: calloc: %s", __func__, strerror(ERRNO)); + closesocket(sock); + } else { + newconn->client.sock = sock; + newconn->client.rsa.u.sin = sin; + if (use_ssl) { + sslize(newconn, SSL_connect); + } + } + } + + return newconn; +} + +// Check whether full request is buffered. Return: +// -1 if request is malformed +// 0 if request is not yet fully buffered +// >0 actual request length, including last \r\n\r\n +static int get_request_len(const char *buf, int buflen) { + const char *s, *e; + int len = 0; + + DEBUG_TRACE(("buf: %p, len: %d", buf, buflen)); + for (s = buf, e = s + buflen - 1; len <= 0 && s < e; s++) + // Control characters are not allowed but >=128 is. + if (!isprint(* (const unsigned char *) s) && *s != '\r' && + *s != '\n' && * (const unsigned char *) s < 128) { + len = -1; + } else if (s[0] == '\n' && s[1] == '\n') { + len = (int) (s - buf) + 2; + } else if (s[0] == '\n' && &s[1] < e && + s[1] == '\r' && s[2] == '\n') { + len = (int) (s - buf) + 3; + } + + return len; +} + +// Convert month to the month number. Return -1 on error, or month number +static int month_number_to_month_name(const char *s) { + size_t i; + + for (i = 0; i < ARRAY_SIZE(month_names); i++) + if (!strcmp(s, month_names[i])) + return (int) i; + + return -1; +} + +// Parse date-time string, and return the corresponding time_t value +static time_t parse_date_string(const char *s) { + time_t current_time; + struct tm tm, *tmp; + char mon[32]; + int sec, min, hour, mday, month, year; + + (void) memset(&tm, 0, sizeof(tm)); + sec = min = hour = mday = month = year = 0; + + if (((sscanf(s, "%d/%3s/%d %d:%d:%d", + &mday, mon, &year, &hour, &min, &sec) == 6) || + (sscanf(s, "%d %3s %d %d:%d:%d", + &mday, mon, &year, &hour, &min, &sec) == 6) || + (sscanf(s, "%*3s, %d %3s %d %d:%d:%d", + &mday, mon, &year, &hour, &min, &sec) == 6) || + (sscanf(s, "%d-%3s-%d %d:%d:%d", + &mday, mon, &year, &hour, &min, &sec) == 6)) && + (month = month_number_to_month_name(mon)) != -1) { + tm.tm_mday = mday; + tm.tm_mon = month; + tm.tm_year = year; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + } + + if (tm.tm_year > 1900) { + tm.tm_year -= 1900; + } else if (tm.tm_year < 70) { + tm.tm_year += 100; + } + + // Set Daylight Saving Time field + current_time = time(NULL); + tmp = localtime(¤t_time); + tm.tm_isdst = tmp->tm_isdst; + + return mktime(&tm); +} + +// Protect against directory disclosure attack by removing '..', +// excessive '/' and '\' characters +static void remove_double_dots_and_double_slashes(char *s) { + char *p = s; + + while (*s != '\0') { + *p++ = *s++; + if (s[-1] == '/' || s[-1] == '\\') { + // Skip all following slashes and backslashes + while (*s == '/' || *s == '\\') { + s++; + } + + // Skip all double-dots + while (*s == '.' && s[1] == '.') { + s += 2; + } + } + } + *p = '\0'; +} + +static const struct { + const char *extension; + size_t ext_len; + const char *mime_type; + size_t mime_type_len; +} builtin_mime_types[] = { + {".html", 5, "text/html", 9}, + {".htm", 4, "text/html", 9}, + {".shtm", 5, "text/html", 9}, + {".shtml", 6, "text/html", 9}, + {".css", 4, "text/css", 8}, + {".js", 3, "application/x-javascript", 24}, + {".ico", 4, "image/x-icon", 12}, + {".gif", 4, "image/gif", 9}, + {".jpg", 4, "image/jpeg", 10}, + {".jpeg", 5, "image/jpeg", 10}, + {".png", 4, "image/png", 9}, + {".svg", 4, "image/svg+xml", 13}, + {".torrent", 8, "application/x-bittorrent", 24}, + {".wav", 4, "audio/x-wav", 11}, + {".mp3", 4, "audio/x-mp3", 11}, + {".mid", 4, "audio/mid", 9}, + {".m3u", 4, "audio/x-mpegurl", 15}, + {".ram", 4, "audio/x-pn-realaudio", 20}, + {".xml", 4, "text/xml", 8}, + {".xslt", 5, "application/xml", 15}, + {".ra", 3, "audio/x-pn-realaudio", 20}, + {".doc", 4, "application/msword", 19}, + {".exe", 4, "application/octet-stream", 24}, + {".zip", 4, "application/x-zip-compressed", 28}, + {".xls", 4, "application/excel", 17}, + {".tgz", 4, "application/x-tar-gz", 20}, + {".tar", 4, "application/x-tar", 17}, + {".gz", 3, "application/x-gunzip", 20}, + {".arj", 4, "application/x-arj-compressed", 28}, + {".rar", 4, "application/x-arj-compressed", 28}, + {".rtf", 4, "application/rtf", 15}, + {".pdf", 4, "application/pdf", 15}, + {".swf", 4, "application/x-shockwave-flash",29}, + {".mpg", 4, "video/mpeg", 10}, + {".mpeg", 5, "video/mpeg", 10}, + {".asf", 4, "video/x-ms-asf", 14}, + {".avi", 4, "video/x-msvideo", 15}, + {".bmp", 4, "image/bmp", 9}, + {NULL, 0, NULL, 0} +}; + +// Look at the "path" extension and figure what mime type it has. +// Store mime type in the vector. +static void get_mime_type(struct mg_context *ctx, const char *path, + struct vec *vec) { + struct vec ext_vec, mime_vec; + const char *list, *ext; + size_t i, path_len; + + path_len = strlen(path); + + // Scan user-defined mime types first, in case user wants to + // override default mime types. + list = ctx->config[EXTRA_MIME_TYPES]; + while ((list = next_option(list, &ext_vec, &mime_vec)) != NULL) { + // ext now points to the path suffix + ext = path + path_len - ext_vec.len; + if (mg_strncasecmp(ext, ext_vec.ptr, ext_vec.len) == 0) { + *vec = mime_vec; + return; + } + } + + // Now scan built-in mime types + for (i = 0; builtin_mime_types[i].extension != NULL; i++) { + ext = path + (path_len - builtin_mime_types[i].ext_len); + if (path_len > builtin_mime_types[i].ext_len && + mg_strcasecmp(ext, builtin_mime_types[i].extension) == 0) { + vec->ptr = builtin_mime_types[i].mime_type; + vec->len = builtin_mime_types[i].mime_type_len; + return; + } + } + + // Nothing found. Fall back to "text/plain" + vec->ptr = "text/plain"; + vec->len = 10; +} + +#ifndef HAVE_MD5 +typedef struct MD5Context { + uint32_t buf[4]; + uint32_t bits[2]; + unsigned char in[64]; +} MD5_CTX; + +#if defined(__BYTE_ORDER) && (__BYTE_ORDER == 1234) +#define byteReverse(buf, len) // Do nothing +#else +static void byteReverse(unsigned char *buf, unsigned longs) { + uint32_t t; + do { + t = (uint32_t) ((unsigned) buf[3] << 8 | buf[2]) << 16 | + ((unsigned) buf[1] << 8 | buf[0]); + *(uint32_t *) buf = t; + buf += 4; + } while (--longs); +} +#endif + +#define F1(x, y, z) (z ^ (x & (y ^ z))) +#define F2(x, y, z) F1(z, x, y) +#define F3(x, y, z) (x ^ y ^ z) +#define F4(x, y, z) (y ^ (x | ~z)) + +#define MD5STEP(f, w, x, y, z, data, s) \ + ( w += f(x, y, z) + data, w = w<<s | w>>(32-s), w += x ) + +// Start MD5 accumulation. Set bit count to 0 and buffer to mysterious +// initialization constants. +static void MD5Init(MD5_CTX *ctx) { + ctx->buf[0] = 0x67452301; + ctx->buf[1] = 0xefcdab89; + ctx->buf[2] = 0x98badcfe; + ctx->buf[3] = 0x10325476; + + ctx->bits[0] = 0; + ctx->bits[1] = 0; +} + +static void MD5Transform(uint32_t buf[4], uint32_t const in[16]) { + register uint32_t a, b, c, d; + + a = buf[0]; + b = buf[1]; + c = buf[2]; + d = buf[3]; + + MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7); + MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12); + MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17); + MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22); + MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7); + MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12); + MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17); + MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22); + MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7); + MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12); + MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17); + MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22); + MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7); + MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12); + MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17); + MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22); + + MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5); + MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9); + MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14); + MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20); + MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5); + MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9); + MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14); + MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20); + MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5); + MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9); + MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14); + MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20); + MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5); + MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9); + MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14); + MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20); + + MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4); + MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11); + MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16); + MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23); + MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4); + MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11); + MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16); + MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23); + MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4); + MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11); + MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16); + MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23); + MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4); + MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11); + MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16); + MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23); + + MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6); + MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10); + MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15); + MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21); + MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6); + MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10); + MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15); + MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21); + MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6); + MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10); + MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15); + MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21); + MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6); + MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10); + MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15); + MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21); + + buf[0] += a; + buf[1] += b; + buf[2] += c; + buf[3] += d; +} + +static void MD5Update(MD5_CTX *ctx, unsigned char const *buf, unsigned len) { + uint32_t t; + + t = ctx->bits[0]; + if ((ctx->bits[0] = t + ((uint32_t) len << 3)) < t) + ctx->bits[1]++; + ctx->bits[1] += len >> 29; + + t = (t >> 3) & 0x3f; + + if (t) { + unsigned char *p = (unsigned char *) ctx->in + t; + + t = 64 - t; + if (len < t) { + memcpy(p, buf, len); + return; + } + memcpy(p, buf, t); + byteReverse(ctx->in, 16); + MD5Transform(ctx->buf, (uint32_t *) ctx->in); + buf += t; + len -= t; + } + + while (len >= 64) { + memcpy(ctx->in, buf, 64); + byteReverse(ctx->in, 16); + MD5Transform(ctx->buf, (uint32_t *) ctx->in); + buf += 64; + len -= 64; + } + + memcpy(ctx->in, buf, len); +} + +static void MD5Final(unsigned char digest[16], MD5_CTX *ctx) { + unsigned count; + unsigned char *p; + + count = (ctx->bits[0] >> 3) & 0x3F; + + p = ctx->in + count; + *p++ = 0x80; + count = 64 - 1 - count; + if (count < 8) { + memset(p, 0, count); + byteReverse(ctx->in, 16); + MD5Transform(ctx->buf, (uint32_t *) ctx->in); + memset(ctx->in, 0, 56); + } else { + memset(p, 0, count - 8); + } + byteReverse(ctx->in, 14); + + ((uint32_t *) ctx->in)[14] = ctx->bits[0]; + ((uint32_t *) ctx->in)[15] = ctx->bits[1]; + + MD5Transform(ctx->buf, (uint32_t *) ctx->in); + byteReverse((unsigned char *) ctx->buf, 4); + memcpy(digest, ctx->buf, 16); + memset((char *) ctx, 0, sizeof(*ctx)); +} +#endif // !HAVE_MD5 + +// Stringify binary data. Output buffer must be twice as big as input, +// because each byte takes 2 bytes in string representation +static void bin2str(char *to, const unsigned char *p, size_t len) { + static const char *hex = "0123456789abcdef"; + + for (; len--; p++) { + *to++ = hex[p[0] >> 4]; + *to++ = hex[p[0] & 0x0f]; + } + *to = '\0'; +} + +// Return stringified MD5 hash for list of vectors. Buffer must be 33 bytes. +void mg_md5(char *buf, ...) { + unsigned char hash[16]; + const char *p; + va_list ap; + MD5_CTX ctx; + + MD5Init(&ctx); + + va_start(ap, buf); + while ((p = va_arg(ap, const char *)) != NULL) { + MD5Update(&ctx, (const unsigned char *) p, (unsigned) strlen(p)); + } + va_end(ap); + + MD5Final(hash, &ctx); + bin2str(buf, hash, sizeof(hash)); +} + +// Check the user's password, return 1 if OK +static int check_password(const char *method, const char *ha1, const char *uri, + const char *nonce, const char *nc, const char *cnonce, + const char *qop, const char *response) { + char ha2[32 + 1], expected_response[32 + 1]; + + // Some of the parameters may be NULL + if (method == NULL || nonce == NULL || nc == NULL || cnonce == NULL || + qop == NULL || response == NULL) { + return 0; + } + + // NOTE(lsm): due to a bug in MSIE, we do not compare the URI + // TODO(lsm): check for authentication timeout + if (// strcmp(dig->uri, c->ouri) != 0 || + strlen(response) != 32 + // || now - strtoul(dig->nonce, NULL, 10) > 3600 + ) { + return 0; + } + + mg_md5(ha2, method, ":", uri, NULL); + mg_md5(expected_response, ha1, ":", nonce, ":", nc, + ":", cnonce, ":", qop, ":", ha2, NULL); + + return mg_strcasecmp(response, expected_response) == 0; +} + +// Use the global passwords file, if specified by auth_gpass option, +// or search for .htpasswd in the requested directory. +static FILE *open_auth_file(struct mg_connection *conn, const char *path) { + struct mg_context *ctx = conn->ctx; + char name[PATH_MAX]; + const char *p, *e; + struct mgstat st; + FILE *fp; + + if (ctx->config[GLOBAL_PASSWORDS_FILE] != NULL) { + // Use global passwords file + fp = mg_fopen(ctx->config[GLOBAL_PASSWORDS_FILE], "r"); + if (fp == NULL) + cry(fc(ctx), "fopen(%s): %s", + ctx->config[GLOBAL_PASSWORDS_FILE], strerror(ERRNO)); + } else if (!mg_stat(path, &st) && st.is_directory) { + (void) mg_snprintf(conn, name, sizeof(name), "%s%c%s", + path, DIRSEP, PASSWORDS_FILE_NAME); + fp = mg_fopen(name, "r"); + } else { + // Try to find .htpasswd in requested directory. + for (p = path, e = p + strlen(p) - 1; e > p; e--) + if (IS_DIRSEP_CHAR(*e)) + break; + (void) mg_snprintf(conn, name, sizeof(name), "%.*s%c%s", + (int) (e - p), p, DIRSEP, PASSWORDS_FILE_NAME); + fp = mg_fopen(name, "r"); + } + + return fp; +} + +// Parsed Authorization header +struct ah { + char *user, *uri, *cnonce, *response, *qop, *nc, *nonce; +}; + +static int parse_auth_header(struct mg_connection *conn, char *buf, + size_t buf_size, struct ah *ah) { + char *name, *value, *s; + const char *auth_header; + + if ((auth_header = mg_get_header(conn, "Authorization")) == NULL || + mg_strncasecmp(auth_header, "Digest ", 7) != 0) { + return 0; + } + + // Make modifiable copy of the auth header + (void) mg_strlcpy(buf, auth_header + 7, buf_size); + + s = buf; + (void) memset(ah, 0, sizeof(*ah)); + + // Parse authorization header + for (;;) { + // Gobble initial spaces + while (isspace(* (unsigned char *) s)) { + s++; + } + name = skip_quoted(&s, "=", " ", 0); + /* Value is either quote-delimited, or ends at first comma or space. */ + if (s[0] == '\"') { + s++; + value = skip_quoted(&s, "\"", " ", '\\'); + if (s[0] == ',') { + s++; + } + } else { + value = skip_quoted(&s, ", ", " ", 0); // IE uses commas, FF uses spaces + } + if (*name == '\0') { + break; + } + + if (!strcmp(name, "username")) { + ah->user = value; + } else if (!strcmp(name, "cnonce")) { + ah->cnonce = value; + } else if (!strcmp(name, "response")) { + ah->response = value; + } else if (!strcmp(name, "uri")) { + ah->uri = value; + } else if (!strcmp(name, "qop")) { + ah->qop = value; + } else if (!strcmp(name, "nc")) { + ah->nc = value; + } else if (!strcmp(name, "nonce")) { + ah->nonce = value; + } + } + + // CGI needs it as REMOTE_USER + if (ah->user != NULL) { + conn->request_info.remote_user = mg_strdup(ah->user); + } else { + return 0; + } + + return 1; +} + +// Authorize against the opened passwords file. Return 1 if authorized. +static int authorize(struct mg_connection *conn, FILE *fp) { + struct ah ah; + char line[256], f_user[256], ha1[256], f_domain[256], buf[BUFSIZ]; + + if (!parse_auth_header(conn, buf, sizeof(buf), &ah)) { + return 0; + } + + // Loop over passwords file + while (fgets(line, sizeof(line), fp) != NULL) { + if (sscanf(line, "%[^:]:%[^:]:%s", f_user, f_domain, ha1) != 3) { + continue; + } + + if (!strcmp(ah.user, f_user) && + !strcmp(conn->ctx->config[AUTHENTICATION_DOMAIN], f_domain)) + return check_password( + conn->request_info.request_method, + ha1, ah.uri, ah.nonce, ah.nc, ah.cnonce, ah.qop, + ah.response); + } + + return 0; +} + +// Return 1 if request is authorised, 0 otherwise. +static int check_authorization(struct mg_connection *conn, const char *path) { + FILE *fp; + char fname[PATH_MAX]; + struct vec uri_vec, filename_vec; + const char *list; + int authorized; + + fp = NULL; + authorized = 1; + + list = conn->ctx->config[PROTECT_URI]; + while ((list = next_option(list, &uri_vec, &filename_vec)) != NULL) { + if (!memcmp(conn->request_info.uri, uri_vec.ptr, uri_vec.len)) { + (void) mg_snprintf(conn, fname, sizeof(fname), "%.*s", + filename_vec.len, filename_vec.ptr); + if ((fp = mg_fopen(fname, "r")) == NULL) { + cry(conn, "%s: cannot open %s: %s", __func__, fname, strerror(errno)); + } + break; + } + } + + if (fp == NULL) { + fp = open_auth_file(conn, path); + } + + if (fp != NULL) { + authorized = authorize(conn, fp); + (void) fclose(fp); + } + + return authorized; +} + +static void send_authorization_request(struct mg_connection *conn) { + conn->request_info.status_code = 401; + (void) mg_printf(conn, + "HTTP/1.1 401 Unauthorized\r\n" + "WWW-Authenticate: Digest qop=\"auth\", " + "realm=\"%s\", nonce=\"%lu\"\r\n\r\n", + conn->ctx->config[AUTHENTICATION_DOMAIN], + (unsigned long) time(NULL)); +} + +static int is_authorized_for_put(struct mg_connection *conn) { + FILE *fp; + int ret = 0; + + fp = conn->ctx->config[PUT_DELETE_PASSWORDS_FILE] == NULL ? NULL : + mg_fopen(conn->ctx->config[PUT_DELETE_PASSWORDS_FILE], "r"); + + if (fp != NULL) { + ret = authorize(conn, fp); + (void) fclose(fp); + } + + return ret; +} + +int mg_modify_passwords_file(const char *fname, const char *domain, + const char *user, const char *pass) { + int found; + char line[512], u[512], d[512], ha1[33], tmp[PATH_MAX]; + FILE *fp, *fp2; + + found = 0; + fp = fp2 = NULL; + + // Regard empty password as no password - remove user record. + if (pass[0] == '\0') { + pass = NULL; + } + + (void) snprintf(tmp, sizeof(tmp), "%s.tmp", fname); + + // Create the file if does not exist + if ((fp = mg_fopen(fname, "a+")) != NULL) { + (void) fclose(fp); + } + + // Open the given file and temporary file + if ((fp = mg_fopen(fname, "r")) == NULL) { + return 0; + } else if ((fp2 = mg_fopen(tmp, "w+")) == NULL) { + fclose(fp); + return 0; + } + + // Copy the stuff to temporary file + while (fgets(line, sizeof(line), fp) != NULL) { + if (sscanf(line, "%[^:]:%[^:]:%*s", u, d) != 2) { + continue; + } + + if (!strcmp(u, user) && !strcmp(d, domain)) { + found++; + if (pass != NULL) { + mg_md5(ha1, user, ":", domain, ":", pass, NULL); + fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); + } + } else { + (void) fprintf(fp2, "%s", line); + } + } + + // If new user, just add it + if (!found && pass != NULL) { + mg_md5(ha1, user, ":", domain, ":", pass, NULL); + (void) fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); + } + + // Close files + (void) fclose(fp); + (void) fclose(fp2); + + // Put the temp file in place of real file + (void) mg_remove(fname); + (void) mg_rename(tmp, fname); + + return 1; +} + +struct de { + struct mg_connection *conn; + char *file_name; + struct mgstat st; +}; + +static void url_encode(const char *src, char *dst, size_t dst_len) { + static const char *dont_escape = "._-$,;~()"; + static const char *hex = "0123456789abcdef"; + const char *end = dst + dst_len - 1; + + for (; *src != '\0' && dst < end; src++, dst++) { + if (isalnum(*(const unsigned char *) src) || + strchr(dont_escape, * (const unsigned char *) src) != NULL) { + *dst = *src; + } else if (dst + 2 < end) { + dst[0] = '%'; + dst[1] = hex[(* (const unsigned char *) src) >> 4]; + dst[2] = hex[(* (const unsigned char *) src) & 0xf]; + dst += 2; + } + } + + *dst = '\0'; +} + +static void print_dir_entry(struct de *de) { + char size[64], mod[64], href[PATH_MAX]; + + if (de->st.is_directory) { + (void) mg_snprintf(de->conn, size, sizeof(size), "%s", "[DIRECTORY]"); + } else { + // We use (signed) cast below because MSVC 6 compiler cannot + // convert unsigned __int64 to double. Sigh. + if (de->st.size < 1024) { + (void) mg_snprintf(de->conn, size, sizeof(size), + "%lu", (unsigned long) de->st.size); + } else if (de->st.size < 1024 * 1024) { + (void) mg_snprintf(de->conn, size, sizeof(size), + "%.1fk", (double) de->st.size / 1024.0); + } else if (de->st.size < 1024 * 1024 * 1024) { + (void) mg_snprintf(de->conn, size, sizeof(size), + "%.1fM", (double) de->st.size / 1048576); + } else { + (void) mg_snprintf(de->conn, size, sizeof(size), + "%.1fG", (double) de->st.size / 1073741824); + } + } + (void) strftime(mod, sizeof(mod), "%d-%b-%Y %H:%M", localtime(&de->st.mtime)); + url_encode(de->file_name, href, sizeof(href)); + de->conn->num_bytes_sent += mg_printf(de->conn, + "<tr><td><a href=\"%s%s%s\">%s%s</a></td>" + "<td> %s</td><td>  %s</td></tr>\n", + de->conn->request_info.uri, href, de->st.is_directory ? "/" : "", + de->file_name, de->st.is_directory ? "/" : "", mod, size); +} + +// This function is called from send_directory() and used for +// sorting directory entries by size, or name, or modification time. +// On windows, __cdecl specification is needed in case if project is built +// with __stdcall convention. qsort always requires __cdels callback. +static int WINCDECL compare_dir_entries(const void *p1, const void *p2) { + const struct de *a = (const struct de *) p1, *b = (const struct de *) p2; + const char *query_string = a->conn->request_info.query_string; + int cmp_result = 0; + + if (query_string == NULL) { + query_string = "na"; + } + + if (a->st.is_directory && !b->st.is_directory) { + return -1; // Always put directories on top + } else if (!a->st.is_directory && b->st.is_directory) { + return 1; // Always put directories on top + } else if (*query_string == 'n') { + cmp_result = strcmp(a->file_name, b->file_name); + } else if (*query_string == 's') { + cmp_result = a->st.size == b->st.size ? 0 : + a->st.size > b->st.size ? 1 : -1; + } else if (*query_string == 'd') { + cmp_result = a->st.mtime == b->st.mtime ? 0 : + a->st.mtime > b->st.mtime ? 1 : -1; + } + + return query_string[1] == 'd' ? -cmp_result : cmp_result; +} + +static void handle_directory_request(struct mg_connection *conn, + const char *dir) { + struct dirent *dp; + DIR *dirp; + struct de *entries = NULL; + char path[PATH_MAX]; + int i, sort_direction, num_entries = 0, arr_size = 128; + + if ((dirp = opendir(dir)) == NULL) { + send_http_error(conn, 500, "Cannot open directory", + "Error: opendir(%s): %s", path, strerror(ERRNO)); + return; + } + + (void) mg_printf(conn, "%s", + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Type: text/html; charset=utf-8\r\n\r\n"); + + sort_direction = conn->request_info.query_string != NULL && + conn->request_info.query_string[1] == 'd' ? 'a' : 'd'; + + while ((dp = readdir(dirp)) != NULL) { + + // Do not show current dir and passwords file + if (!strcmp(dp->d_name, ".") || + !strcmp(dp->d_name, "..") || + !strcmp(dp->d_name, PASSWORDS_FILE_NAME)) + continue; + + if (entries == NULL || num_entries >= arr_size) { + arr_size *= 2; + entries = (struct de *) realloc(entries, + arr_size * sizeof(entries[0])); + } + + if (entries == NULL) { + closedir(dirp); + send_http_error(conn, 500, "Cannot open directory", + "%s", "Error: cannot allocate memory"); + return; + } + + mg_snprintf(conn, path, sizeof(path), "%s%c%s", dir, DIRSEP, dp->d_name); + + // If we don't memset stat structure to zero, mtime will have + // garbage and strftime() will segfault later on in + // print_dir_entry(). memset is required only if mg_stat() + // fails. For more details, see + // http://code.google.com/p/mongoose/issues/detail?id=79 + if (mg_stat(path, &entries[num_entries].st) != 0) { + memset(&entries[num_entries].st, 0, sizeof(entries[num_entries].st)); + } + + entries[num_entries].conn = conn; + entries[num_entries].file_name = mg_strdup(dp->d_name); + num_entries++; + } + (void) closedir(dirp); + + conn->num_bytes_sent += mg_printf(conn, + "<html><head><title>Index of %s" + "" + "

Index of %s

"
+      ""
+      ""
+      ""
+      "",
+      conn->request_info.uri, conn->request_info.uri,
+      sort_direction, sort_direction, sort_direction);
+
+  // Print first entry - link to a parent directory
+  conn->num_bytes_sent += mg_printf(conn,
+      ""
+      "\n",
+      conn->request_info.uri, "..", "Parent directory", "-", "-");
+
+  // Sort and print directory entries
+  qsort(entries, (size_t)num_entries, sizeof(entries[0]), compare_dir_entries);
+  for (i = 0; i < num_entries; i++) {
+    print_dir_entry(&entries[i]);
+    free(entries[i].file_name);
+  }
+  free(entries);
+
+  conn->num_bytes_sent += mg_printf(conn, "%s", "
NameModifiedSize

%s %s  %s
"); + conn->request_info.status_code = 200; +} + +// Send len bytes from the opened file to the client. +static void send_file_data(struct mg_connection *conn, FILE *fp, int64_t len) { + char buf[BUFSIZ]; + int to_read, num_read, num_written; + + while (len > 0) { + // Calculate how much to read from the file in the buffer + to_read = sizeof(buf); + if ((int64_t) to_read > len) + to_read = (int) len; + + // Read from file, exit the loop on error + if ((num_read = fread(buf, 1, (size_t)to_read, fp)) == 0) + break; + + // Send read bytes to the client, exit the loop on error + if ((num_written = mg_write(conn, buf, (size_t)num_read)) != num_read) + break; + + // Both read and were successful, adjust counters + conn->num_bytes_sent += num_written; + len -= num_written; + } +} + +static int parse_range_header(const char *header, int64_t *a, int64_t *b) { + return sscanf(header, "bytes=%" INT64_FMT "-%" INT64_FMT, a, b); +} + +static void handle_file_request(struct mg_connection *conn, const char *path, + struct mgstat *stp) { + char date[64], lm[64], etag[64], range[64]; + const char *fmt = "%a, %d %b %Y %H:%M:%S %Z", *msg = "OK", *hdr; + time_t curtime = time(NULL); + int64_t cl, r1, r2; + struct vec mime_vec; + FILE *fp; + int n; + + get_mime_type(conn->ctx, path, &mime_vec); + cl = stp->size; + conn->request_info.status_code = 200; + range[0] = '\0'; + + if ((fp = mg_fopen(path, "rb")) == NULL) { + send_http_error(conn, 500, http_500_error, + "fopen(%s): %s", path, strerror(ERRNO)); + return; + } + set_close_on_exec(fileno(fp)); + + // If Range: header specified, act accordingly + r1 = r2 = 0; + hdr = mg_get_header(conn, "Range"); + if (hdr != NULL && (n = parse_range_header(hdr, &r1, &r2)) > 0) { + conn->request_info.status_code = 206; + (void) fseeko(fp, (off_t) r1, SEEK_SET); + cl = n == 2 ? r2 - r1 + 1: cl - r1; + (void) mg_snprintf(conn, range, sizeof(range), + "Content-Range: bytes " + "%" INT64_FMT "-%" + INT64_FMT "/%" INT64_FMT "\r\n", + r1, r1 + cl - 1, stp->size); + msg = "Partial Content"; + } + + // Prepare Etag, Date, Last-Modified headers + (void) strftime(date, sizeof(date), fmt, localtime(&curtime)); + (void) strftime(lm, sizeof(lm), fmt, localtime(&stp->mtime)); + (void) mg_snprintf(conn, etag, sizeof(etag), "%lx.%lx", + (unsigned long) stp->mtime, (unsigned long) stp->size); + + (void) mg_printf(conn, + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Last-Modified: %s\r\n" + "Etag: \"%s\"\r\n" + "Content-Type: %.*s\r\n" + "Content-Length: %" INT64_FMT "\r\n" + "Connection: %s\r\n" + "Accept-Ranges: bytes\r\n" + "%s\r\n", + conn->request_info.status_code, msg, date, lm, etag, + mime_vec.len, mime_vec.ptr, cl, suggest_connection_header(conn), range); + + if (strcmp(conn->request_info.request_method, "HEAD") != 0) { + send_file_data(conn, fp, cl); + } + (void) fclose(fp); +} + +// Parse HTTP headers from the given buffer, advance buffer to the point +// where parsing stopped. +static void parse_http_headers(char **buf, struct mg_request_info *ri) { + int i; + + for (i = 0; i < (int) ARRAY_SIZE(ri->http_headers); i++) { + ri->http_headers[i].name = skip_quoted(buf, ":", " ", 0); + ri->http_headers[i].value = skip(buf, "\r\n"); + if (ri->http_headers[i].name[0] == '\0') + break; + ri->num_headers = i + 1; + } +} + +static int is_valid_http_method(const char *method) { + return !strcmp(method, "GET") || !strcmp(method, "POST") || + !strcmp(method, "HEAD") || !strcmp(method, "CONNECT") || + !strcmp(method, "PUT") || !strcmp(method, "DELETE"); +} + +// Parse HTTP request, fill in mg_request_info structure. +static int parse_http_request(char *buf, struct mg_request_info *ri) { + int status = 0; + + // RFC says that all initial whitespaces should be ingored + while (*buf != '\0' && isspace(* (unsigned char *) buf)) { + buf++; + } + + ri->request_method = skip(&buf, " "); + ri->uri = skip(&buf, " "); + ri->http_version = skip(&buf, "\r\n"); + + if (is_valid_http_method(ri->request_method) && + strncmp(ri->http_version, "HTTP/", 5) == 0) { + ri->http_version += 5; /* Skip "HTTP/" */ + parse_http_headers(&buf, ri); + status = 1; + } + + return status; +} + +// Keep reading the input (either opened file descriptor fd, or socket sock, +// or SSL descriptor ssl) into buffer buf, until \r\n\r\n appears in the +// buffer (which marks the end of HTTP request). Buffer buf may already +// have some data. The length of the data is stored in nread. +// Upon every read operation, increase nread by the number of bytes read. +static int read_request(FILE *fp, SOCKET sock, SSL *ssl, char *buf, int bufsiz, + int *nread) { + int n, request_len; + + request_len = 0; + while (*nread < bufsiz && request_len == 0) { + n = pull(fp, sock, ssl, buf + *nread, bufsiz - *nread); + if (n <= 0) { + break; + } else { + *nread += n; + request_len = get_request_len(buf, *nread); + } + } + + return request_len; +} + +// For given directory path, substitute it to valid index file. +// Return 0 if index file has been found, -1 if not found. +// If the file is found, it's stats is returned in stp. +static int substitute_index_file(struct mg_connection *conn, char *path, + size_t path_len, struct mgstat *stp) { + const char *list = conn->ctx->config[INDEX_FILES]; + struct mgstat st; + struct vec filename_vec; + size_t n = strlen(path); + int found = 0; + + // The 'path' given to us points to the directory. Remove all trailing + // directory separator characters from the end of the path, and + // then append single directory separator character. + while (n > 0 && IS_DIRSEP_CHAR(path[n - 1])) { + n--; + } + path[n] = DIRSEP; + + // Traverse index files list. For each entry, append it to the given + // path and see if the file exists. If it exists, break the loop + while ((list = next_option(list, &filename_vec, NULL)) != NULL) { + + // Ignore too long entries that may overflow path buffer + if (filename_vec.len > path_len - n) + continue; + + // Prepare full path to the index file + (void) mg_strlcpy(path + n + 1, filename_vec.ptr, filename_vec.len + 1); + + // Does it exist? + if (mg_stat(path, &st) == 0) { + // Yes it does, break the loop + *stp = st; + found = 1; + break; + } + } + + // If no index file exists, restore directory path + if (!found) { + path[n] = '\0'; + } + + return found; +} + +// Return True if we should reply 304 Not Modified. +static int is_not_modified(const struct mg_connection *conn, + const struct mgstat *stp) { + const char *ims = mg_get_header(conn, "If-Modified-Since"); + return ims != NULL && stp->mtime <= parse_date_string(ims); +} + +static int forward_body_data(struct mg_connection *conn, FILE *fp, + SOCKET sock, SSL *ssl) { + const char *expect, *buffered; + char buf[BUFSIZ]; + int to_read, nread, buffered_len, success = 0; + + expect = mg_get_header(conn, "Expect"); + assert(fp != NULL); + + if (conn->content_len == -1) { + send_http_error(conn, 411, "Length Required", ""); + } else if (expect != NULL && mg_strcasecmp(expect, "100-continue")) { + send_http_error(conn, 417, "Expectation Failed", ""); + } else { + if (expect != NULL) { + (void) mg_printf(conn, "%s", "HTTP/1.1 100 Continue\r\n\r\n"); + } + + buffered = conn->buf + conn->request_len; + buffered_len = conn->data_len - conn->request_len; + assert(buffered_len >= 0); + assert(conn->consumed_content == 0); + + if (buffered_len > 0) { + if ((int64_t) buffered_len > conn->content_len) { + buffered_len = (int) conn->content_len; + } + push(fp, sock, ssl, buffered, (int64_t) buffered_len); + conn->consumed_content += buffered_len; + } + + while (conn->consumed_content < conn->content_len) { + to_read = sizeof(buf); + if ((int64_t) to_read > conn->content_len - conn->consumed_content) { + to_read = (int) (conn->content_len - conn->consumed_content); + } + nread = pull(NULL, conn->client.sock, conn->ssl, buf, to_read); + if (nread <= 0 || push(fp, sock, ssl, buf, nread) != nread) { + break; + } + conn->consumed_content += nread; + } + + if (conn->consumed_content == conn->content_len) { + success = 1; + } + + // Each error code path in this function must send an error + if (!success) { + send_http_error(conn, 577, http_500_error, ""); + } + } + + return success; +} + +#if !defined(NO_CGI) +// This structure helps to create an environment for the spawned CGI program. +// Environment is an array of "VARIABLE=VALUE\0" ASCIIZ strings, +// last element must be NULL. +// However, on Windows there is a requirement that all these VARIABLE=VALUE\0 +// strings must reside in a contiguous buffer. The end of the buffer is +// marked by two '\0' characters. +// We satisfy both worlds: we create an envp array (which is vars), all +// entries are actually pointers inside buf. +struct cgi_env_block { + struct mg_connection *conn; + char buf[CGI_ENVIRONMENT_SIZE]; // Environment buffer + int len; // Space taken + char *vars[MAX_CGI_ENVIR_VARS]; // char **envp + int nvars; // Number of variables +}; + +// Append VARIABLE=VALUE\0 string to the buffer, and add a respective +// pointer into the vars array. +static char *addenv(struct cgi_env_block *block, const char *fmt, ...) { + int n, space; + char *added; + va_list ap; + + // Calculate how much space is left in the buffer + space = sizeof(block->buf) - block->len - 2; + assert(space >= 0); + + // Make a pointer to the free space int the buffer + added = block->buf + block->len; + + // Copy VARIABLE=VALUE\0 string into the free space + va_start(ap, fmt); + n = mg_vsnprintf(block->conn, added, (size_t) space, fmt, ap); + va_end(ap); + + // Make sure we do not overflow buffer and the envp array + if (n > 0 && n < space && + block->nvars < (int) ARRAY_SIZE(block->vars) - 2) { + // Append a pointer to the added string into the envp array + block->vars[block->nvars++] = block->buf + block->len; + // Bump up used length counter. Include \0 terminator + block->len += n + 1; + } + + return added; +} + +static void prepare_cgi_environment(struct mg_connection *conn, + const char *prog, + struct cgi_env_block *blk) { + const char *s, *slash; + struct vec var_vec, root; + char *p; + int i; + + blk->len = blk->nvars = 0; + blk->conn = conn; + + get_document_root(conn, &root); + + addenv(blk, "SERVER_NAME=%s", conn->ctx->config[AUTHENTICATION_DOMAIN]); + addenv(blk, "SERVER_ROOT=%.*s", root.len, root.ptr); + addenv(blk, "DOCUMENT_ROOT=%.*s", root.len, root.ptr); + + // Prepare the environment block + addenv(blk, "%s", "GATEWAY_INTERFACE=CGI/1.1"); + addenv(blk, "%s", "SERVER_PROTOCOL=HTTP/1.1"); + addenv(blk, "%s", "REDIRECT_STATUS=200"); // For PHP + addenv(blk, "SERVER_PORT=%d", ntohs(conn->client.lsa.u.sin.sin_port)); + addenv(blk, "REQUEST_METHOD=%s", conn->request_info.request_method); + addenv(blk, "REMOTE_ADDR=%s", + inet_ntoa(conn->client.rsa.u.sin.sin_addr)); + addenv(blk, "REMOTE_PORT=%d", conn->request_info.remote_port); + addenv(blk, "REQUEST_URI=%s", conn->request_info.uri); + + // SCRIPT_NAME + assert(conn->request_info.uri[0] == '/'); + slash = strrchr(conn->request_info.uri, '/'); + if ((s = strrchr(prog, '/')) == NULL) + s = prog; + addenv(blk, "SCRIPT_NAME=%.*s%s", slash - conn->request_info.uri, + conn->request_info.uri, s); + + addenv(blk, "SCRIPT_FILENAME=%s", prog); + addenv(blk, "PATH_TRANSLATED=%s", prog); + addenv(blk, "HTTPS=%s", conn->ssl == NULL ? "off" : "on"); + + if ((s = mg_get_header(conn, "Content-Type")) != NULL) + addenv(blk, "CONTENT_TYPE=%s", s); + + if (conn->request_info.query_string != NULL) + addenv(blk, "QUERY_STRING=%s", conn->request_info.query_string); + + if ((s = mg_get_header(conn, "Content-Length")) != NULL) + addenv(blk, "CONTENT_LENGTH=%s", s); + + if ((s = getenv("PATH")) != NULL) + addenv(blk, "PATH=%s", s); + +#if defined(_WIN32) + if ((s = getenv("COMSPEC")) != NULL) + addenv(blk, "COMSPEC=%s", s); + if ((s = getenv("SYSTEMROOT")) != NULL) + addenv(blk, "SYSTEMROOT=%s", s); +#else + if ((s = getenv("LD_LIBRARY_PATH")) != NULL) + addenv(blk, "LD_LIBRARY_PATH=%s", s); +#endif /* _WIN32 */ + + if ((s = getenv("PERLLIB")) != NULL) + addenv(blk, "PERLLIB=%s", s); + + if (conn->request_info.remote_user != NULL) { + addenv(blk, "REMOTE_USER=%s", conn->request_info.remote_user); + addenv(blk, "%s", "AUTH_TYPE=Digest"); + } + + // Add all headers as HTTP_* variables + for (i = 0; i < conn->request_info.num_headers; i++) { + p = addenv(blk, "HTTP_%s=%s", + conn->request_info.http_headers[i].name, + conn->request_info.http_headers[i].value); + + // Convert variable name into uppercase, and change - to _ + for (; *p != '=' && *p != '\0'; p++) { + if (*p == '-') + *p = '_'; + *p = (char) toupper(* (unsigned char *) p); + } + } + + // Add user-specified variables + s = conn->ctx->config[CGI_ENVIRONMENT]; + while ((s = next_option(s, &var_vec, NULL)) != NULL) { + addenv(blk, "%.*s", var_vec.len, var_vec.ptr); + } + + blk->vars[blk->nvars++] = NULL; + blk->buf[blk->len++] = '\0'; + + assert(blk->nvars < (int) ARRAY_SIZE(blk->vars)); + assert(blk->len > 0); + assert(blk->len < (int) sizeof(blk->buf)); +} + +static void handle_cgi_request(struct mg_connection *conn, const char *prog) { + int headers_len, data_len, i, fd_stdin[2], fd_stdout[2]; + const char *status; + char buf[BUFSIZ], *pbuf, dir[PATH_MAX], *p; + struct mg_request_info ri; + struct cgi_env_block blk; + FILE *in, *out; + pid_t pid; + + prepare_cgi_environment(conn, prog, &blk); + + // CGI must be executed in its own directory. 'dir' must point to the + // directory containing executable program, 'p' must point to the + // executable program name relative to 'dir'. + (void) mg_snprintf(conn, dir, sizeof(dir), "%s", prog); + if ((p = strrchr(dir, DIRSEP)) != NULL) { + *p++ = '\0'; + } else { + dir[0] = '.', dir[1] = '\0'; + p = (char *) prog; + } + + pid = (pid_t) -1; + fd_stdin[0] = fd_stdin[1] = fd_stdout[0] = fd_stdout[1] = -1; + in = out = NULL; + + if (pipe(fd_stdin) != 0 || pipe(fd_stdout) != 0) { + send_http_error(conn, 500, http_500_error, + "Cannot create CGI pipe: %s", strerror(ERRNO)); + goto done; + } else if ((pid = spawn_process(conn, p, blk.buf, blk.vars, + fd_stdin[0], fd_stdout[1], dir)) == (pid_t) -1) { + goto done; + } else if ((in = fdopen(fd_stdin[1], "wb")) == NULL || + (out = fdopen(fd_stdout[0], "rb")) == NULL) { + send_http_error(conn, 500, http_500_error, + "fopen: %s", strerror(ERRNO)); + goto done; + } + + setbuf(in, NULL); + setbuf(out, NULL); + + // spawn_process() must close those! + // If we don't mark them as closed, close() attempt before + // return from this function throws an exception on Windows. + // Windows does not like when closed descriptor is closed again. + fd_stdin[0] = fd_stdout[1] = -1; + + // Send POST data to the CGI process if needed + if (!strcmp(conn->request_info.request_method, "POST") && + !forward_body_data(conn, in, INVALID_SOCKET, NULL)) { + goto done; + } + + // Now read CGI reply into a buffer. We need to set correct + // status code, thus we need to see all HTTP headers first. + // Do not send anything back to client, until we buffer in all + // HTTP headers. + data_len = 0; + headers_len = read_request(out, INVALID_SOCKET, NULL, + buf, sizeof(buf), &data_len); + if (headers_len <= 0) { + send_http_error(conn, 500, http_500_error, + "CGI program sent malformed HTTP headers: [%.*s]", + data_len, buf); + goto done; + } + pbuf = buf; + buf[headers_len - 1] = '\0'; + parse_http_headers(&pbuf, &ri); + + // Make up and send the status line + status = get_header(&ri, "Status"); + conn->request_info.status_code = status == NULL ? 200 : atoi(status); + (void) mg_printf(conn, "HTTP/1.1 %d OK\r\n", conn->request_info.status_code); + + // Send headers + for (i = 0; i < ri.num_headers; i++) { + mg_printf(conn, "%s: %s\r\n", + ri.http_headers[i].name, ri.http_headers[i].value); + } + (void) mg_write(conn, "\r\n", 2); + + // Send chunk of data that may be read after the headers + conn->num_bytes_sent += mg_write(conn, buf + headers_len, + (size_t)(data_len - headers_len)); + + // Read the rest of CGI output and send to the client + send_file_data(conn, out, INT64_MAX); + +done: + if (pid != (pid_t) -1) { + kill(pid, SIGKILL); +#if !defined(_WIN32) + do {} while (waitpid(-1, &i, WNOHANG) > 0); +#endif + } + if (fd_stdin[0] != -1) { + (void) close(fd_stdin[0]); + } + if (fd_stdout[1] != -1) { + (void) close(fd_stdout[1]); + } + + if (in != NULL) { + (void) fclose(in); + } else if (fd_stdin[1] != -1) { + (void) close(fd_stdin[1]); + } + + if (out != NULL) { + (void) fclose(out); + } else if (fd_stdout[0] != -1) { + (void) close(fd_stdout[0]); + } +} +#endif // !NO_CGI + +// For a given PUT path, create all intermediate subdirectories +// for given path. Return 0 if the path itself is a directory, +// or -1 on error, 1 if OK. +static int put_dir(const char *path) { + char buf[PATH_MAX]; + const char *s, *p; + struct mgstat st; + size_t len; + + for (s = p = path + 2; (p = strchr(s, '/')) != NULL; s = ++p) { + len = p - path; + assert(len < sizeof(buf)); + (void) memcpy(buf, path, len); + buf[len] = '\0'; + + // Try to create intermediate directory + if (mg_stat(buf, &st) == -1 && mg_mkdir(buf, 0755) != 0) { + return -1; + } + + // Is path itself a directory? + if (p[1] == '\0') { + return 0; + } + } + + return 1; +} + +static void put_file(struct mg_connection *conn, const char *path) { + struct mgstat st; + const char *range; + int64_t r1, r2; + FILE *fp; + int rc; + + conn->request_info.status_code = mg_stat(path, &st) == 0 ? 200 : 201; + + if ((rc = put_dir(path)) == 0) { + mg_printf(conn, "HTTP/1.1 %d OK\r\n\r\n", conn->request_info.status_code); + } else if (rc == -1) { + send_http_error(conn, 500, http_500_error, + "put_dir(%s): %s", path, strerror(ERRNO)); + } else if ((fp = mg_fopen(path, "wb+")) == NULL) { + send_http_error(conn, 500, http_500_error, + "fopen(%s): %s", path, strerror(ERRNO)); + } else { + set_close_on_exec(fileno(fp)); + range = mg_get_header(conn, "Content-Range"); + r1 = r2 = 0; + if (range != NULL && parse_range_header(range, &r1, &r2) > 0) { + conn->request_info.status_code = 206; + // TODO(lsm): handle seek error + (void) fseeko(fp, (off_t) r1, SEEK_SET); + } + if (forward_body_data(conn, fp, INVALID_SOCKET, NULL)) + (void) mg_printf(conn, "HTTP/1.1 %d OK\r\n\r\n", + conn->request_info.status_code); + (void) fclose(fp); + } +} + +static void send_ssi_file(struct mg_connection *, const char *, FILE *, int); + +static void do_ssi_include(struct mg_connection *conn, const char *ssi, + char *tag, int include_level) { + char file_name[BUFSIZ], path[PATH_MAX], *p; + struct vec root; + int is_ssi; + FILE *fp; + + get_document_root(conn, &root); + + // sscanf() is safe here, since send_ssi_file() also uses buffer + // of size BUFSIZ to get the tag. So strlen(tag) is always < BUFSIZ. + if (sscanf(tag, " virtual=\"%[^\"]\"", file_name) == 1) { + // File name is relative to the webserver root + (void) mg_snprintf(conn, path, sizeof(path), "%.*s%c%s", + root.len, root.ptr, DIRSEP, file_name); + } else if (sscanf(tag, " file=\"%[^\"]\"", file_name) == 1) { + // File name is relative to the webserver working directory + // or it is absolute system path + (void) mg_snprintf(conn, path, sizeof(path), "%s", file_name); + } else if (sscanf(tag, " \"%[^\"]\"", file_name) == 1) { + // File name is relative to the currect document + (void) mg_snprintf(conn, path, sizeof(path), "%s", ssi); + if ((p = strrchr(path, DIRSEP)) != NULL) { + p[1] = '\0'; + } + (void) mg_snprintf(conn, path + strlen(path), + sizeof(path) - strlen(path), "%s", file_name); + } else { + cry(conn, "Bad SSI #include: [%s]", tag); + return; + } + + if ((fp = mg_fopen(path, "rb")) == NULL) { + cry(conn, "Cannot open SSI #include: [%s]: fopen(%s): %s", + tag, path, strerror(ERRNO)); + } else { + set_close_on_exec(fileno(fp)); + is_ssi = match_extension(path, conn->ctx->config[SSI_EXTENSIONS]); + if (is_ssi) { + send_ssi_file(conn, path, fp, include_level + 1); + } else { + send_file_data(conn, fp, INT64_MAX); + } + (void) fclose(fp); + } +} + +#if !defined(NO_POPEN) +static void do_ssi_exec(struct mg_connection *conn, char *tag) { + char cmd[BUFSIZ]; + FILE *fp; + + if (sscanf(tag, " \"%[^\"]\"", cmd) != 1) { + cry(conn, "Bad SSI #exec: [%s]", tag); + } else if ((fp = popen(cmd, "r")) == NULL) { + cry(conn, "Cannot SSI #exec: [%s]: %s", cmd, strerror(ERRNO)); + } else { + send_file_data(conn, fp, INT64_MAX); + (void) pclose(fp); + } +} +#endif // !NO_POPEN + +static void send_ssi_file(struct mg_connection *conn, const char *path, + FILE *fp, int include_level) { + char buf[BUFSIZ]; + int ch, len, in_ssi_tag; + + if (include_level > 10) { + cry(conn, "SSI #include level is too deep (%s)", path); + return; + } + + in_ssi_tag = 0; + len = 0; + + while ((ch = fgetc(fp)) != EOF) { + if (in_ssi_tag && ch == '>') { + in_ssi_tag = 0; + buf[len++] = (char) ch; + buf[len] = '\0'; + assert(len <= (int) sizeof(buf)); + if (len < 6 || memcmp(buf, " + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/ghostdriver-test/fixtures/common/actualXhtmlPage.xhtml b/test/ghostdriver-test/fixtures/common/actualXhtmlPage.xhtml new file mode 100644 index 0000000..a0f5470 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/actualXhtmlPage.xhtml @@ -0,0 +1,14 @@ + + + + + + Title + + + +

+ Foo +

+ + diff --git a/test/ghostdriver-test/fixtures/common/ajaxy_page.html b/test/ghostdriver-test/fixtures/common/ajaxy_page.html new file mode 100644 index 0000000..4b34031 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/ajaxy_page.html @@ -0,0 +1,81 @@ + + + +
+ + +
+ + Red + Green +
+ +
+ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/alerts.html b/test/ghostdriver-test/fixtures/common/alerts.html new file mode 100644 index 0000000..1add0db --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/alerts.html @@ -0,0 +1,85 @@ + + + + + Testing Alerts + + + + + +

Testing Alerts and Stuff

+ +

This tests alerts: click me

+ +

This tests alerts: click me

+ +

Let's make the prompt happen

+ +

Let's make the prompt with default happen

+ +

Let's make TWO prompts happen

+ +

A SLOW alert

+ +

This is a test of a confirm: + test confirm

+ +

This is a test of showModalDialog: test dialog

+ +

This is a test of an alert in an iframe: + +

+ +

This is a test of an alert in a nested iframe: + +

+ +

This is a test of an alert open from onload event handler: open new page

+ +

This is a test of an alert open from onload event handler: open new window

+ +

This is a test of an alert open from onunload event handler: open new page

+ +

This is a test of an alert open from onclose event handler: open new window

+ +

This is a test of an alert open from onclose event handler: open new window

+ +
+
+
+ +

+

+ + diff --git a/test/ghostdriver-test/fixtures/common/animals/.gitignore b/test/ghostdriver-test/fixtures/common/animals/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/common/banner.gif b/test/ghostdriver-test/fixtures/common/banner.gif new file mode 100644 index 0000000..3f34354 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/banner.gif differ diff --git a/test/ghostdriver-test/fixtures/common/beach.jpg b/test/ghostdriver-test/fixtures/common/beach.jpg new file mode 100644 index 0000000..402237c Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/beach.jpg differ diff --git a/test/ghostdriver-test/fixtures/common/blank.html b/test/ghostdriver-test/fixtures/common/blank.html new file mode 100644 index 0000000..c3f376e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/blank.html @@ -0,0 +1 @@ +blank diff --git a/test/ghostdriver-test/fixtures/common/bodyTypingTest.html b/test/ghostdriver-test/fixtures/common/bodyTypingTest.html new file mode 100644 index 0000000..f2b1939 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/bodyTypingTest.html @@ -0,0 +1,41 @@ + + + + Testing Typing into body + + + + +

Type Stuff

+ +
+   +
+ +
+   +
+ + + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/booleanAttributes.html b/test/ghostdriver-test/fixtures/common/booleanAttributes.html new file mode 100644 index 0000000..16fbbe9 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/booleanAttributes.html @@ -0,0 +1,19 @@ + + + Elements with boolean attributes + + +
+ + + + + +
+ + +
+ +
Unwrappable text
+ + diff --git a/test/ghostdriver-test/fixtures/common/child/childPage.html b/test/ghostdriver-test/fixtures/common/child/childPage.html new file mode 100644 index 0000000..9192b54 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/child/childPage.html @@ -0,0 +1,8 @@ + + + Depth one child page + + +

I'm a page in a child directory

+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/child/grandchild/grandchildPage.html b/test/ghostdriver-test/fixtures/common/child/grandchild/grandchildPage.html new file mode 100644 index 0000000..f52685e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/child/grandchild/grandchildPage.html @@ -0,0 +1,8 @@ + + + Depth two child page + + +

I'm a page in a grandchild directory! How cute!

+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/clickEventPage.html b/test/ghostdriver-test/fixtures/common/clickEventPage.html new file mode 100644 index 0000000..8e0355d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/clickEventPage.html @@ -0,0 +1,26 @@ + + + + Testing click events + + + +
+ Click me to view my coordinates +
+ +
+

 

+
+ + diff --git a/test/ghostdriver-test/fixtures/common/click_frames.html b/test/ghostdriver-test/fixtures/common/click_frames.html new file mode 100644 index 0000000..bd055c7 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_frames.html @@ -0,0 +1,10 @@ + + + click frames + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_jacker.html b/test/ghostdriver-test/fixtures/common/click_jacker.html new file mode 100644 index 0000000..0ff3900 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_jacker.html @@ -0,0 +1,38 @@ + + + + click-jacking + + + +
+
Click jacked!
+
Click Me
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_out_of_bounds.html b/test/ghostdriver-test/fixtures/common/click_out_of_bounds.html new file mode 100644 index 0000000..8a51659 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_out_of_bounds.html @@ -0,0 +1,23 @@ + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+ + + diff --git a/test/ghostdriver-test/fixtures/common/click_out_of_bounds_overflow.html b/test/ghostdriver-test/fixtures/common/click_out_of_bounds_overflow.html new file mode 100644 index 0000000..15ac17f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_out_of_bounds_overflow.html @@ -0,0 +1,85 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
data
click me
+
+ diff --git a/test/ghostdriver-test/fixtures/common/click_rtl.html b/test/ghostdriver-test/fixtures/common/click_rtl.html new file mode 100644 index 0000000..e84fffa --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_rtl.html @@ -0,0 +1,19 @@ + + +RTL test + + + +
+ +
مفتاح معايير الويب
+ +
פעילות הבינאום
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_source.html b/test/ghostdriver-test/fixtures/common/click_source.html new file mode 100644 index 0000000..22e9319 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_source.html @@ -0,0 +1,18 @@ + + + Click Source + + + I go to a target + + + + + + Click Source + + + I go to a target + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_tests/google_map.html b/test/ghostdriver-test/fixtures/common/click_tests/google_map.html new file mode 100644 index 0000000..eb2e556 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/google_map.html @@ -0,0 +1,15 @@ + + + + Google Image Map + + +

Google Image Map

+ + +area 1 +area 2 +area 3 + + + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/google_map.png b/test/ghostdriver-test/fixtures/common/click_tests/google_map.png new file mode 100644 index 0000000..763f562 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/click_tests/google_map.png differ diff --git a/test/ghostdriver-test/fixtures/common/click_tests/html5_submit_buttons.html b/test/ghostdriver-test/fixtures/common/click_tests/html5_submit_buttons.html new file mode 100644 index 0000000..5ec11ee --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/html5_submit_buttons.html @@ -0,0 +1,15 @@ + + + +HTML5 Submit Buttons + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_tests/issue5237.html b/test/ghostdriver-test/fixtures/common/click_tests/issue5237.html new file mode 100644 index 0000000..464fa11 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/issue5237.html @@ -0,0 +1,9 @@ + + + + Sample page for issue 5237 + + + + + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/issue5237_frame.html b/test/ghostdriver-test/fixtures/common/click_tests/issue5237_frame.html new file mode 100644 index 0000000..d6f4caf --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/issue5237_frame.html @@ -0,0 +1 @@ +Continue \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_tests/issue5237_target.html b/test/ghostdriver-test/fixtures/common/click_tests/issue5237_target.html new file mode 100644 index 0000000..cbc16e8 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/issue5237_target.html @@ -0,0 +1,10 @@ + + + + Target page for issue 5237 + + +

Test passed

+ + + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/link_that_wraps.html b/test/ghostdriver-test/fixtures/common/click_tests/link_that_wraps.html new file mode 100644 index 0000000..0443436 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/link_that_wraps.html @@ -0,0 +1,11 @@ + + + + Link that continues on next line + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_tests/mapped_page1.html b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page1.html new file mode 100644 index 0000000..245f038 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page1.html @@ -0,0 +1,9 @@ + + + + Target Page 1 + + +
Target Page 1
+ + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/mapped_page2.html b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page2.html new file mode 100644 index 0000000..6f9636c --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page2.html @@ -0,0 +1,9 @@ + + + + Target Page 2 + + +
Target Page 2
+ + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/mapped_page3.html b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page3.html new file mode 100644 index 0000000..87a35f3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/mapped_page3.html @@ -0,0 +1,9 @@ + + + + Target Page 3 + + +
Target Page 3
+ + diff --git a/test/ghostdriver-test/fixtures/common/click_tests/span_that_wraps.html b/test/ghostdriver-test/fixtures/common/click_tests/span_that_wraps.html new file mode 100644 index 0000000..77a9d6d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/span_that_wraps.html @@ -0,0 +1,11 @@ + + + + Link that continues on next line + + +
+
placeholder
Span that continues on next line +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_tests/submitted_page.html b/test/ghostdriver-test/fixtures/common/click_tests/submitted_page.html new file mode 100644 index 0000000..0ed2cba --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_tests/submitted_page.html @@ -0,0 +1,9 @@ + + + +Submitted Successfully! + + +

Submitted Successfully!

+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/click_too_big.html b/test/ghostdriver-test/fixtures/common/click_too_big.html new file mode 100644 index 0000000..568ee77 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_too_big.html @@ -0,0 +1,10 @@ + + + + +
+       +
+
+ + diff --git a/test/ghostdriver-test/fixtures/common/click_too_big_in_frame.html b/test/ghostdriver-test/fixtures/common/click_too_big_in_frame.html new file mode 100644 index 0000000..cda990e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/click_too_big_in_frame.html @@ -0,0 +1,11 @@ + + + This page has iframes + + +

This is the heading

+ + + + +
+I'm a normal link +
+I go to an anchor +
+I open a window with javascript +
+Click me +
+ +
+I'm a green link +

looooooooooong short looooooooooong +

+ +333333 +

I have a span

And another span

+ + diff --git a/test/ghostdriver-test/fixtures/common/closeable_window.html b/test/ghostdriver-test/fixtures/common/closeable_window.html new file mode 100644 index 0000000..e64c599 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/closeable_window.html @@ -0,0 +1,8 @@ + + +closeable window + + +This window can be closed by clicking on this. + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/cn-test.html b/test/ghostdriver-test/fixtures/common/cn-test.html new file mode 100644 index 0000000..df846ad --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/cn-test.html @@ -0,0 +1,156 @@ + + + + + + + + + + +
+
+ + +

չ2008ƣӿ


+
+ 882008˻ᵹʱһף찲Ź㳡СͼΪףеݳ » + + ӿġҪһԤ⣬ҪԽһʶ
+
+й֮
+
+
+ +
+ +
+ + + diff --git a/test/ghostdriver-test/fixtures/common/colorPage.html b/test/ghostdriver-test/fixtures/common/colorPage.html new file mode 100644 index 0000000..0d1bfc0 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/colorPage.html @@ -0,0 +1,20 @@ + + + + Color Page + + +
namedColor
+
rgb
+
rgbpct
+
hex
+
hex
+
hsl
+
rgba
+
rgba
+
hsla
+ + + + + diff --git a/test/ghostdriver-test/fixtures/common/cookies.html b/test/ghostdriver-test/fixtures/common/cookies.html new file mode 100644 index 0000000..7db5b49 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/cookies.html @@ -0,0 +1,30 @@ + + + Testing cookies + + + + +

Cookie Mashing

+ .com Click
+ . Click
+ google.com Click
+ .google.com Click
+ 127.0.0.1 Click
+ localhost:3001 Click
+ .google:3001 Click
+ 172.16.12.225 Click
+ 172.16.12.225:port Click
+ Set on a different path + +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_frame.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_frame.html new file mode 100644 index 0000000..7714a48 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_frame.html @@ -0,0 +1,9 @@ + + + + Welcome Page + + + + + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_nested_frame.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_nested_frame.html new file mode 100644 index 0000000..b3143b0 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/element_in_nested_frame.html @@ -0,0 +1,9 @@ + + + + Welcome Page + + + + + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_element_out_of_view.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_element_out_of_view.html new file mode 100644 index 0000000..6f2bcd4 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_element_out_of_view.html @@ -0,0 +1,11 @@ + + + + Page With Element Out Of View + + +
Placeholder
+
Red box
+
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_empty_element.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_empty_element.html new file mode 100644 index 0000000..b07972a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_empty_element.html @@ -0,0 +1,10 @@ + + + + Page With Empty Element + + +
+
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_fixed_element.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_fixed_element.html new file mode 100644 index 0000000..b815891 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_fixed_element.html @@ -0,0 +1,12 @@ + + + + Page With Fixed Element + + +
fixed red box
+
Placeholder
+
Element at the bottom
+
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_hidden_element.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_hidden_element.html new file mode 100644 index 0000000..286b04b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_hidden_element.html @@ -0,0 +1,10 @@ + + + + Page With Hidden Element + + + +
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_invisible_element.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_invisible_element.html new file mode 100644 index 0000000..dc33c71 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_invisible_element.html @@ -0,0 +1,10 @@ + + + + Page With Invisible Element + + + +
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_transparent_element.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_transparent_element.html new file mode 100644 index 0000000..d0090d9 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/page_with_transparent_element.html @@ -0,0 +1,10 @@ + + + + Page With Transparent Element + + +
Hidden box
+
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/coordinates_tests/simple_page.html b/test/ghostdriver-test/fixtures/common/coordinates_tests/simple_page.html new file mode 100644 index 0000000..7b857b9 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/coordinates_tests/simple_page.html @@ -0,0 +1,10 @@ + + + + Simple Page + + +
Red box
+
Tex after box
+ + diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png new file mode 100644 index 0000000..954e22d Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png new file mode 100644 index 0000000..64ece57 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png new file mode 100644 index 0000000..abdc010 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png new file mode 100644 index 0000000..9b383f4 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png new file mode 100644 index 0000000..a23baad Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 0000000..42ccba2 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png new file mode 100644 index 0000000..39d5824 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png new file mode 100644 index 0000000..f127367 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png new file mode 100644 index 0000000..359397a Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_222222_256x240.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_222222_256x240.png new file mode 100644 index 0000000..b273ff1 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_222222_256x240.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_228ef1_256x240.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_228ef1_256x240.png new file mode 100644 index 0000000..a641a37 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_228ef1_256x240.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ef8c08_256x240.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ef8c08_256x240.png new file mode 100644 index 0000000..85e63e9 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ef8c08_256x240.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffd27a_256x240.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffd27a_256x240.png new file mode 100644 index 0000000..e117eff Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffd27a_256x240.png differ diff --git a/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffffff_256x240.png b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000..42f8f99 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/css/ui-lightness/images/ui-icons_ffffff_256x240.png differ diff --git a/test/ghostdriver-test/fixtures/common/cssTransform.html b/test/ghostdriver-test/fixtures/common/cssTransform.html new file mode 100644 index 0000000..c3b9964 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/cssTransform.html @@ -0,0 +1,61 @@ + + +
+You shouldn't see anything other than this sentence on the page +
+
+ I have a hidden child +
+ I am a hidden child +
+
+
+ I have a hidden child +
+ I am a hidden child +
+
+
I am a hidden element
+
I am a hidden element
diff --git a/test/ghostdriver-test/fixtures/common/cssTransform2.html b/test/ghostdriver-test/fixtures/common/cssTransform2.html new file mode 100644 index 0000000..602924b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/cssTransform2.html @@ -0,0 +1,20 @@ + + +
+
+
+
+
+
+
I am not a hidden element
diff --git a/test/ghostdriver-test/fixtures/common/document_write_in_onload.html b/test/ghostdriver-test/fixtures/common/document_write_in_onload.html new file mode 100644 index 0000000..a15fc47 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/document_write_in_onload.html @@ -0,0 +1,13 @@ + + + Document Write In Onload + + + +

hello world

+ + diff --git a/test/ghostdriver-test/fixtures/common/dragAndDropInsideScrolledDiv.html b/test/ghostdriver-test/fixtures/common/dragAndDropInsideScrolledDiv.html new file mode 100644 index 0000000..0b2ee9a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/dragAndDropInsideScrolledDiv.html @@ -0,0 +1,67 @@ + + + + + + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/dragAndDropTest.html b/test/ghostdriver-test/fixtures/common/dragAndDropTest.html new file mode 100644 index 0000000..fdee16b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/dragAndDropTest.html @@ -0,0 +1,102 @@ + + + + + + + + +
+
+"Hi there +
+
+
+
+ + diff --git a/test/ghostdriver-test/fixtures/common/dragDropOverflow.html b/test/ghostdriver-test/fixtures/common/dragDropOverflow.html new file mode 100644 index 0000000..ecb2562 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/dragDropOverflow.html @@ -0,0 +1,104 @@ + + + + + + +
+
+
+
+
12am
+
1am
+
2am
+
3am
+
4am
+
5am
+
6am
+
7am
+
8am
+
9am
+
10am
+
11am
+
12pm
+
1pm
+
2pm
+
3pm
+
4pm
+
5pm
+
6pm
+
7pm
+
8pm
+
9pm
+
10pm
+
11pm
+
+
+
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/draggableLists.html b/test/ghostdriver-test/fixtures/common/draggableLists.html new file mode 100644 index 0000000..f7e0dca --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/draggableLists.html @@ -0,0 +1,67 @@ + + + + + jQuery UI Sortable - Connect lists + + + + + + + + + +
+
    +
  • LeftItem 1
  • +
  • LeftItem 2
  • +
  • LeftItem 3
  • +
  • LeftItem 4
  • +
  • LeftItem 5
  • +
+ +
    +
  • RightItem 1
  • +
  • RightItem 2
  • +
  • RightItem 3
  • +
  • RightItem 4
  • +
  • RightItem 5
  • +
+ +
+ +
+
+

Nothing happened.

+
+ + + diff --git a/test/ghostdriver-test/fixtures/common/droppableItems.html b/test/ghostdriver-test/fixtures/common/droppableItems.html new file mode 100644 index 0000000..fc850ac --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/droppableItems.html @@ -0,0 +1,65 @@ + + + + + jQuery UI Droppable - Default Demo + + + + + + + +
+ +
+

Drag me to my target

+
+ +
+

Drop here

+
+ +
+

start

+
+ +
+ +
+ +

Taken from the JQuery demo.

+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/dynamic.html b/test/ghostdriver-test/fixtures/common/dynamic.html new file mode 100644 index 0000000..b9e6067 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/dynamic.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/dynamicallyModifiedPage.html b/test/ghostdriver-test/fixtures/common/dynamicallyModifiedPage.html new file mode 100644 index 0000000..ed7c7ed --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/dynamicallyModifiedPage.html @@ -0,0 +1,42 @@ + + + + Delayed remove of an element + + + + + +
+ +
+

element

+ + diff --git a/test/ghostdriver-test/fixtures/common/errors.html b/test/ghostdriver-test/fixtures/common/errors.html new file mode 100644 index 0000000..78fb902 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/errors.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/ghostdriver-test/fixtures/common/fixedFooterNoScroll.html b/test/ghostdriver-test/fixtures/common/fixedFooterNoScroll.html new file mode 100644 index 0000000..ca65d1f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/fixedFooterNoScroll.html @@ -0,0 +1,13 @@ + + + + Fixed footer with no scrollbar + + +
+
+ Click me +
+
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/fixedFooterNoScrollQuirksMode.html b/test/ghostdriver-test/fixtures/common/fixedFooterNoScrollQuirksMode.html new file mode 100644 index 0000000..2593bf3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/fixedFooterNoScrollQuirksMode.html @@ -0,0 +1,12 @@ + + + Fixed footer with no scrollbar + + +
+
+ Click me +
+
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/formPage.html b/test/ghostdriver-test/fixtures/common/formPage.html new file mode 100644 index 0000000..e1197db --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/formPage.html @@ -0,0 +1,173 @@ + + + We Leave From Here + + + + +There should be a form here: + +
+ + +
+ +
+ +
+ +
+ Here's a checkbox: + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + Cheese
+ Peas
+ Cheese and peas
+ Not a sausage
+ Not another sausage + + + +

I like cheese

+ + + Cumberland sausage +
+ +
+ + + + + + + + +
+ +
+ + + + + + + +
+ +
+ + + + + + + +
+ + +
+
+ +
+ +
+ + +
+

+ + + +

+
+ +
+ + + +
+

+ +

+
+ + + diff --git a/test/ghostdriver-test/fixtures/common/formSelectionPage.html b/test/ghostdriver-test/fixtures/common/formSelectionPage.html new file mode 100644 index 0000000..4890c08 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/formSelectionPage.html @@ -0,0 +1,46 @@ + + + + Testing Typing into body + + + + +

Type Stuff

+ +
+   +
+ +
+ +
+ + + + diff --git a/test/ghostdriver-test/fixtures/common/form_handling_js_submit.html b/test/ghostdriver-test/fixtures/common/form_handling_js_submit.html new file mode 100644 index 0000000..3023143 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/form_handling_js_submit.html @@ -0,0 +1,30 @@ + + + + + + Form with JS action + + +
+ +
+ +

+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/framePage3.html b/test/ghostdriver-test/fixtures/common/framePage3.html new file mode 100644 index 0000000..3e62e45 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/framePage3.html @@ -0,0 +1,7 @@ + + + inner + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/frameScrollChild.html b/test/ghostdriver-test/fixtures/common/frameScrollChild.html new file mode 100644 index 0000000..3eb3bf4 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frameScrollChild.html @@ -0,0 +1,26 @@ + + + + Child frame + + +

This is a scrolling frame test

+
+ + + + + + + + + + + + + +
First row
Second row
Third row
Fourth row
+
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/frameScrollPage.html b/test/ghostdriver-test/fixtures/common/frameScrollPage.html new file mode 100644 index 0000000..b7fb8f2 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frameScrollPage.html @@ -0,0 +1,14 @@ + + + + Welcome Page + + +
+ +
+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/frameScrollParent.html b/test/ghostdriver-test/fixtures/common/frameScrollParent.html new file mode 100644 index 0000000..8fccb6d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frameScrollParent.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876.html b/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876.html new file mode 100644 index 0000000..4ed597d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876.html @@ -0,0 +1,9 @@ + + + +Test issue 4876 + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876_iframe.html b/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876_iframe.html new file mode 100644 index 0000000..57d47d8 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frame_switching_tests/bug4876_iframe.html @@ -0,0 +1,9 @@ + + + +
+ + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame.html b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame.html new file mode 100644 index 0000000..9c27e04 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame.html @@ -0,0 +1,29 @@ + + + Deleting frame: main page + + + + +
+ + +
+
+ +
+ + + diff --git a/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe.html b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe.html new file mode 100644 index 0000000..e4b9723 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe.html @@ -0,0 +1,8 @@ + + + Deleting frame: iframe + + + + + diff --git a/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe2.html b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe2.html new file mode 100644 index 0000000..47764eb --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frame_switching_tests/deletingFrame_iframe2.html @@ -0,0 +1,7 @@ + + + Deleting frame: iframe 2 + + +
Added back
+ diff --git a/test/ghostdriver-test/fixtures/common/frameset.html b/test/ghostdriver-test/fixtures/common/frameset.html new file mode 100644 index 0000000..039c5f2 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/frameset.html @@ -0,0 +1,14 @@ + + + Unique title + + + + + + + + + + + diff --git a/test/ghostdriver-test/fixtures/common/framesetPage2.html b/test/ghostdriver-test/fixtures/common/framesetPage2.html new file mode 100644 index 0000000..4ea35ff --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/framesetPage2.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/framesetPage3.html b/test/ghostdriver-test/fixtures/common/framesetPage3.html new file mode 100644 index 0000000..42a9300 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/framesetPage3.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/galaxy/.gitignore b/test/ghostdriver-test/fixtures/common/galaxy/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/common/globalscope.html b/test/ghostdriver-test/fixtures/common/globalscope.html new file mode 100644 index 0000000..e4ca97a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/globalscope.html @@ -0,0 +1,15 @@ + + + + Global scope + + + +
+ + diff --git a/test/ghostdriver-test/fixtures/common/hidden.html b/test/ghostdriver-test/fixtures/common/hidden.html new file mode 100644 index 0000000..0e8097e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/hidden.html @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/html5/blue.jpg b/test/ghostdriver-test/fixtures/common/html5/blue.jpg new file mode 100644 index 0000000..8ea27c4 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/html5/blue.jpg differ diff --git a/test/ghostdriver-test/fixtures/common/html5/database.js b/test/ghostdriver-test/fixtures/common/html5/database.js new file mode 100644 index 0000000..c6333be --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5/database.js @@ -0,0 +1,84 @@ +var database={}; +database.db={}; + +database.onError = function(tx, e) { + var log = document.createElement('div'); + log.setAttribute('name','error'); + log.setAttribute('style','background-color:red'); + log.innerText = e.message; + document.getElementById('logs').appendChild(log); +} + +database.onSuccess = function(tx, r) { + if (r.rows.length) { + var ol; + for (var i = 0; i < r.rows.length; i++) { + ol = document.createElement('ol'); + ol.innerHTML = r.rows.item(i).ID + ": " + r.rows.item(i).docname + " (" + r.rows.item(i).created + ")"; + document.getElementById('logs').appendChild(ol); + } + + } +} + +database.open=function(){ + database.db=openDatabase('HTML5', '1.0', 'Offline document storage', 100*1024); +} + +database.create=function(){ + database.db.transaction(function(tx) { + tx.executeSql("CREATE TABLE IF NOT EXISTS docs(ID INTEGER PRIMARY KEY ASC, docname TEXT, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", + [], + database.onSuccess, + database.onError); + });} + +database.add = function(message) { + database.db.transaction(function(tx){ + tx.executeSql("INSERT INTO docs(docname) VALUES (?)", + [message], database.onSuccess, database.onError); + }); +} + +database.selectAll = function() { + database.db.transaction(function(tx) { + tx.executeSql("SELECT * FROM docs", [], database.onSuccess, + database.onError); + }); +} + +database.onDeleteAllSuccess = function(tx, r) { + var doc = document.documentElement; + var db_completed = document.createElement("div"); + db_completed.setAttribute("id", "db_completed"); + db_completed.innerText = "db operation completed"; + doc.appendChild(db_completed); +} + +database.deleteAll = function() { + database.db.transaction(function(tx) { + tx.executeSql("delete from docs", [], database.onDeleteAllSuccess, + database.onError); + }); +} + +var log = document.createElement('div'); +log.setAttribute('name','notice'); +log.setAttribute('style','background-color:yellow'); +log.innerText = typeof window.openDatabase == "function" ? "Web Database is supported." : "Web Database is not supported."; +document.getElementById('logs').appendChild(log); + +try { + database.open(); + database.create(); + database.add('Doc 1'); + database.add('Doc 2'); + database.selectAll(); + database.deleteAll(); +} catch(error) { + var log = document.createElement('div'); + log.setAttribute('name','critical'); + log.setAttribute('style','background-color:pink'); + log.innerText = error; + document.getElementById('logs').appendChild(log); +} diff --git a/test/ghostdriver-test/fixtures/common/html5/geolocation.js b/test/ghostdriver-test/fixtures/common/html5/geolocation.js new file mode 100644 index 0000000..f07af14 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5/geolocation.js @@ -0,0 +1,18 @@ +function success(position) { + var message = document.getElementById("status"); + message.innerHTML =""; + message.innerHTML += "

Longitude: " + position.coords.longitude + "

"; + message.innerHTML += "

Latitude: " + position.coords.latitude + "

"; + message.innerHTML += "

Altitude: " + position.coords.altitude + "

"; +} + +function error(msg) { + var message = document.getElementById("status"); + message.innerHTML = "Failed to get geolocation."; +} + +if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(success, error); +} else { + error('Geolocation is not supported.'); +} \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/html5/green.jpg b/test/ghostdriver-test/fixtures/common/html5/green.jpg new file mode 100644 index 0000000..6a0d3be Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/html5/green.jpg differ diff --git a/test/ghostdriver-test/fixtures/common/html5/offline.html b/test/ghostdriver-test/fixtures/common/html5/offline.html new file mode 100644 index 0000000..c24178b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5/offline.html @@ -0,0 +1 @@ +Offline diff --git a/test/ghostdriver-test/fixtures/common/html5/red.jpg b/test/ghostdriver-test/fixtures/common/html5/red.jpg new file mode 100644 index 0000000..f296e27 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/html5/red.jpg differ diff --git a/test/ghostdriver-test/fixtures/common/html5/status.html b/test/ghostdriver-test/fixtures/common/html5/status.html new file mode 100644 index 0000000..394116a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5/status.html @@ -0,0 +1 @@ +Online diff --git a/test/ghostdriver-test/fixtures/common/html5/test.appcache b/test/ghostdriver-test/fixtures/common/html5/test.appcache new file mode 100644 index 0000000..3bc4e00 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5/test.appcache @@ -0,0 +1,11 @@ +CACHE MANIFEST + +CACHE: +# Additional items to cache. +yellow.jpg +red.jpg +blue.jpg +green.jpg + +FALLBACK: +status.html offline.html diff --git a/test/ghostdriver-test/fixtures/common/html5/yellow.jpg b/test/ghostdriver-test/fixtures/common/html5/yellow.jpg new file mode 100644 index 0000000..7c609b3 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/html5/yellow.jpg differ diff --git a/test/ghostdriver-test/fixtures/common/html5Page.html b/test/ghostdriver-test/fixtures/common/html5Page.html new file mode 100644 index 0000000..355ddc3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/html5Page.html @@ -0,0 +1,32 @@ + + +HTML5 + + + +

Geolocation Test

+
Location unknown
+ + +

Web SQL Database Test

+
+ + +

Application Cache Test

+
+

Current network status:

+ + + + + +
+ + + diff --git a/test/ghostdriver-test/fixtures/common/icon.gif b/test/ghostdriver-test/fixtures/common/icon.gif new file mode 100644 index 0000000..bb99461 Binary files /dev/null and b/test/ghostdriver-test/fixtures/common/icon.gif differ diff --git a/test/ghostdriver-test/fixtures/common/iframeAtBottom.html b/test/ghostdriver-test/fixtures/common/iframeAtBottom.html new file mode 100644 index 0000000..a686ba3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/iframeAtBottom.html @@ -0,0 +1,15 @@ + + + This page has iframes + + +

This is the heading

+ +
+ diff --git a/test/ghostdriver-test/fixtures/common/iframes.html b/test/ghostdriver-test/fixtures/common/iframes.html new file mode 100644 index 0000000..e00b482 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/iframes.html @@ -0,0 +1,11 @@ + + + This page has iframes + + +

This is the heading

+ + + + diff --git a/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_1.html b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_1.html new file mode 100644 index 0000000..4eff01a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_1.html @@ -0,0 +1,21 @@ + + +First Modal + + + + +

Modal dialog sample

+ + + +lnk2 +
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_2.html b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_2.html new file mode 100644 index 0000000..cec3f3f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_2.html @@ -0,0 +1,21 @@ + + +Second Modal + + + + +

Modal dialog sample

+ + + +lnk3 +
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_3.html b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_3.html new file mode 100644 index 0000000..6c5eb72 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/modal_dialogs/modal_3.html @@ -0,0 +1,15 @@ + + +Third Modal + + + + +

Modal dialog sample

+ + +
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/modal_dialogs/modalindex.html b/test/ghostdriver-test/fixtures/common/modal_dialogs/modalindex.html new file mode 100644 index 0000000..0a1c4c9 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/modal_dialogs/modalindex.html @@ -0,0 +1,21 @@ + + +Main window + + + + +

Modal dialog sample

+ + + +lnk1 +
+ +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/mouseOver.html b/test/ghostdriver-test/fixtures/common/mouseOver.html new file mode 100644 index 0000000..d4751bf --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/mouseOver.html @@ -0,0 +1,17 @@ + + +
+
+
+ +
diff --git a/test/ghostdriver-test/fixtures/common/mousePositionTracker.html b/test/ghostdriver-test/fixtures/common/mousePositionTracker.html new file mode 100644 index 0000000..39a31cd --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/mousePositionTracker.html @@ -0,0 +1,33 @@ + + + + + + + + + Div tracking mouse position. +
+
+ Move mouse here. +
+

+0, 0 +

+ + diff --git a/test/ghostdriver-test/fixtures/common/nestedElements.html b/test/ghostdriver-test/fixtures/common/nestedElements.html new file mode 100644 index 0000000..cf00083 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/nestedElements.html @@ -0,0 +1,155 @@ + + +

outside

+

outside

+
+

inside

+
+ + Here's a checkbox:
+ + + +
+ Here's a checkbox:
+ + +
+ +hello world + + + + + + +
+ Here's a checkbox:
+ +
+ +
+ Here's a checkbox:
+ + +
+ +hello world + + + + + + +
+ Here's a checkbox:
+ +
+ +
+ Here's a checkbox:
+ + +
+ +hello world + + + + + + +
+ Here's a checkbox:
+ +
+ +
+ Here's a checkbox:
+ + +
+ +hello world + + +Span with class of one +
+ Find me + Also me + But not me +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/overflow-body.html b/test/ghostdriver-test/fixtures/common/overflow-body.html new file mode 100644 index 0000000..2d2264c --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow-body.html @@ -0,0 +1,15 @@ + + + + The Visibility of Everyday Things + + + +

This image is copyright Simon Stewart and donated to the Selenium project for use in its test suites. +

+a nice beach + + + + + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_auto.html b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_auto.html new file mode 100644 index 0000000..cf8a647 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_auto.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_hidden.html b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_hidden.html new file mode 100644 index 0000000..96fd750 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_hidden.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_scroll.html b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_scroll.html new file mode 100644 index 0000000..6f1d90b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_auto_y_scroll.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_auto.html b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_auto.html new file mode 100644 index 0000000..24dd192 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_auto.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_hidden.html b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_hidden.html new file mode 100644 index 0000000..cae5665 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_hidden.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_scroll.html b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_scroll.html new file mode 100644 index 0000000..d4ffa39 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_hidden_y_scroll.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_auto.html b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_auto.html new file mode 100644 index 0000000..d425a2a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_auto.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_hidden.html b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_hidden.html new file mode 100644 index 0000000..4a6ff59 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_hidden.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_scroll.html b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_scroll.html new file mode 100644 index 0000000..efa8074 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/overflow/x_scroll_y_scroll.html @@ -0,0 +1,30 @@ + + + + Page with overflow + + + +
+ +
+ Right clicked:
+ Bottom clicked:
+ Bottom-right clicked:
+
+ + Click bottom +
+ + diff --git a/test/ghostdriver-test/fixtures/common/pageWithOnBeforeUnloadMessage.html b/test/ghostdriver-test/fixtures/common/pageWithOnBeforeUnloadMessage.html new file mode 100644 index 0000000..cb59707 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/pageWithOnBeforeUnloadMessage.html @@ -0,0 +1,20 @@ + + + + + + Page with OnBeforeUnload handler + + +

Page with onbeforeunload event handler. Click here to navigate to another page.

+ + diff --git a/test/ghostdriver-test/fixtures/common/pageWithOnLoad.html b/test/ghostdriver-test/fixtures/common/pageWithOnLoad.html new file mode 100644 index 0000000..2c644ff --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/pageWithOnLoad.html @@ -0,0 +1,6 @@ + + + +

Page with onload event handler

+ + diff --git a/test/ghostdriver-test/fixtures/common/pageWithOnUnload.html b/test/ghostdriver-test/fixtures/common/pageWithOnUnload.html new file mode 100644 index 0000000..6070341 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/pageWithOnUnload.html @@ -0,0 +1,6 @@ + + + +

Page with onunload event handler

+ + diff --git a/test/ghostdriver-test/fixtures/common/plain.txt b/test/ghostdriver-test/fixtures/common/plain.txt new file mode 100644 index 0000000..8318c86 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/plain.txt @@ -0,0 +1 @@ +Test \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/proxy/page1.html b/test/ghostdriver-test/fixtures/common/proxy/page1.html new file mode 100644 index 0000000..1810f1c --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/proxy/page1.html @@ -0,0 +1,20 @@ + + + +Page 1 +

The next query param must be the URL for the next page to +link to. + diff --git a/test/ghostdriver-test/fixtures/common/proxy/page2.html b/test/ghostdriver-test/fixtures/common/proxy/page2.html new file mode 100644 index 0000000..d826f17 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/proxy/page2.html @@ -0,0 +1,24 @@ + + + +Page 2 +This page is a middle man for referrer tests. +This page will include a link to a "next" page if the next query +parameter, or the document.referrer is set. +

+ diff --git a/test/ghostdriver-test/fixtures/common/proxy/page3.html b/test/ghostdriver-test/fixtures/common/proxy/page3.html new file mode 100644 index 0000000..27048f7 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/proxy/page3.html @@ -0,0 +1,5 @@ + + + +Page 3 +

diff --git a/test/ghostdriver-test/fixtures/common/readOnlyPage.html b/test/ghostdriver-test/fixtures/common/readOnlyPage.html new file mode 100644 index 0000000..b3f0012 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/readOnlyPage.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + +

This is a contentEditable area
+ + + diff --git a/test/ghostdriver-test/fixtures/common/rectangles.html b/test/ghostdriver-test/fixtures/common/rectangles.html new file mode 100644 index 0000000..8ba2339 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/rectangles.html @@ -0,0 +1,40 @@ + + + + Rectangles + + + +
r1
+
r2
+
r3
+ + diff --git a/test/ghostdriver-test/fixtures/common/resultPage.html b/test/ghostdriver-test/fixtures/common/resultPage.html new file mode 100644 index 0000000..94f3e24 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/resultPage.html @@ -0,0 +1,25 @@ + + + We Arrive Here + + + +

Success!

+ +
+

List of stuff

+
    +
  1. Item 1
  2. +
  3. Item 2
  4. +
+
+
+

Almost empty

+
+ + + + + diff --git a/test/ghostdriver-test/fixtures/common/rich_text.html b/test/ghostdriver-test/fixtures/common/rich_text.html new file mode 100644 index 0000000..8c9a073 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/rich_text.html @@ -0,0 +1,160 @@ + + + + + + +
+
+
+ + + + + +
+
IFRAME
+ +
+
frame.contentWindow.document.designMode: on
frame.contentWindow.document.body.contentEditable: false
+
+
DIV
+
+
+
+
div.ownerDocument.designMode: off
div.ownerDocument.body.contentEditable: false
div.contentEditable: true
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + +
type:[]
tagName:[]
id:[]
keyIdentifier:[]
keyLocation:[]
keyCode:[]
charCode:[]
which:[]
isTrusted:[]
---------------------
Modifiers
alt:[]
ctrl:[]
shift:[]
meta:[]
+
+ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/safari/frames_benchmark.html b/test/ghostdriver-test/fixtures/common/safari/frames_benchmark.html new file mode 100644 index 0000000..8a05925 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/safari/frames_benchmark.html @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen.css b/test/ghostdriver-test/fixtures/common/screen/screen.css new file mode 100644 index 0000000..8152618 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen.css @@ -0,0 +1,19 @@ +* { + margin: 0; +} +html, body, #output { + width: 100%; + height: 100%; +} +table { + border: 0px; + border-collapse: collapse; + border-spacing: 0px; + display: table; +} +table td { + padding: 0px; +} +.cell { + color: black; +} diff --git a/test/ghostdriver-test/fixtures/common/screen/screen.html b/test/ghostdriver-test/fixtures/common/screen/screen.html new file mode 100644 index 0000000..166665d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen.js b/test/ghostdriver-test/fixtures/common/screen/screen.js new file mode 100644 index 0000000..1d16859 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen.js @@ -0,0 +1,7 @@ +function toColor(num) { + num >>>= 0; + var b = num & 0xFF, + g = (num & 0xFF00) >>> 8, + r = (num & 0xFF0000) >>> 16; + return "rgb(" + [r, g, b].join(",") + ")"; +} \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_frame1.html b/test/ghostdriver-test/fixtures/common/screen/screen_frame1.html new file mode 100644 index 0000000..d50c21d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_frame1.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_frame2.html b/test/ghostdriver-test/fixtures/common/screen/screen_frame2.html new file mode 100644 index 0000000..b66cd70 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_frame2.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_frames.html b/test/ghostdriver-test/fixtures/common/screen/screen_frames.html new file mode 100644 index 0000000..46852dc --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_frames.html @@ -0,0 +1,11 @@ + + + screen test + + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_iframes.html b/test/ghostdriver-test/fixtures/common/screen/screen_iframes.html new file mode 100644 index 0000000..ae3ea1e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_iframes.html @@ -0,0 +1,12 @@ + + +Screen test + + +
+ + + +
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_too_long.html b/test/ghostdriver-test/fixtures/common/screen/screen_too_long.html new file mode 100644 index 0000000..4d00f02 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_too_long.html @@ -0,0 +1,68 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_x_long.html b/test/ghostdriver-test/fixtures/common/screen/screen_x_long.html new file mode 100644 index 0000000..1a6a100 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_x_long.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_x_too_long.html b/test/ghostdriver-test/fixtures/common/screen/screen_x_too_long.html new file mode 100644 index 0000000..3fee005 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_x_too_long.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_y_long.html b/test/ghostdriver-test/fixtures/common/screen/screen_y_long.html new file mode 100644 index 0000000..31733e0 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_y_long.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/screen/screen_y_too_long.html b/test/ghostdriver-test/fixtures/common/screen/screen_y_too_long.html new file mode 100644 index 0000000..dbef936 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/screen/screen_y_too_long.html @@ -0,0 +1,72 @@ + + +screen test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
     
     
     
     
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/scroll.html b/test/ghostdriver-test/fixtures/common/scroll.html new file mode 100644 index 0000000..cd5214f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scroll.html @@ -0,0 +1,27 @@ + + + + + +
+
    +
  • line1
  • +
  • line2
  • +
  • line3
  • +
  • line4
  • +
  • line5
  • +
  • line6
  • +
  • line7
  • +
  • line8
  • +
  • line9
  • +
+
+Clicked: +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scroll2.html b/test/ghostdriver-test/fixtures/common/scroll2.html new file mode 100644 index 0000000..0ea66d3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scroll2.html @@ -0,0 +1,21 @@ + + + + +
    +
  • +
  • +
  • Text
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scroll3.html b/test/ghostdriver-test/fixtures/common/scroll3.html new file mode 100644 index 0000000..1aa1709 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scroll3.html @@ -0,0 +1,8 @@ + + +



























































































































































+ +



+ +                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 +
























































































































































\ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/scroll4.html b/test/ghostdriver-test/fixtures/common/scroll4.html new file mode 100644 index 0000000..652a778 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scroll4.html @@ -0,0 +1,7 @@ + + +


































































































+ +


































































































+ + diff --git a/test/ghostdriver-test/fixtures/common/scroll5.html b/test/ghostdriver-test/fixtures/common/scroll5.html new file mode 100644 index 0000000..b345a8c --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scroll5.html @@ -0,0 +1,18 @@ + + + + + +
+
+
+
+
+Clicked: +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_200.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_200.html new file mode 100644 index 0000000..3eb3bf4 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_200.html @@ -0,0 +1,26 @@ + + + + Child frame + + +

This is a scrolling frame test

+
+ + + + + + + + + + + + + +
First row
Second row
Third row
Fourth row
+
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_2000.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_2000.html new file mode 100644 index 0000000..61ffe85 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_height_above_2000.html @@ -0,0 +1,26 @@ + + + + Child frame + + +

This is a tall frame test

+
+ + + + + + + + + + + + + +
First row
Second row
Third row
Fourth row
+
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame.html new file mode 100644 index 0000000..1530138 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html new file mode 100644 index 0000000..5781aeb --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_nested_scrolling_frame_out_of_view.html @@ -0,0 +1,12 @@ + + + + Welcome Page + + +
Placeholder
+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_small_height.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_small_height.html new file mode 100644 index 0000000..047de0f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/frame_with_small_height.html @@ -0,0 +1,10 @@ + + + + Child frame + + +

This is a non-scrolling frame test

+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_double_overflow_auto.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_double_overflow_auto.html new file mode 100644 index 0000000..01b7c30 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_double_overflow_auto.html @@ -0,0 +1,19 @@ + + + + Page with overflow: auto + + + +
Placeholder
+
+ Click me! +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_frame_out_of_view.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_frame_out_of_view.html new file mode 100644 index 0000000..c536e41 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_frame_out_of_view.html @@ -0,0 +1,12 @@ + + + + Welcome Page + + +
Placeholder
+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames.html new file mode 100644 index 0000000..e5b7602 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames_out_of_view.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames_out_of_view.html new file mode 100644 index 0000000..f79f7c8 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_nested_scrolling_frames_out_of_view.html @@ -0,0 +1,12 @@ + + + + Welcome Page + + +
Placeholder
+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_non_scrolling_frame.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_non_scrolling_frame.html new file mode 100644 index 0000000..0a493fa --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_non_scrolling_frame.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame.html new file mode 100644 index 0000000..cb5d53a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame_out_of_view.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame_out_of_view.html new file mode 100644 index 0000000..5df1115 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_scrolling_frame_out_of_view.html @@ -0,0 +1,12 @@ + + + + Welcome Page + + +
Placeholder
+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_tall_frame.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_tall_frame.html new file mode 100644 index 0000000..b7cfaf5 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_tall_frame.html @@ -0,0 +1,11 @@ + + + + Welcome Page + + +
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_y_overflow_auto.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_y_overflow_auto.html new file mode 100644 index 0000000..b5716e7 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/page_with_y_overflow_auto.html @@ -0,0 +1,14 @@ + + + + Page with overflow: auto + + +
+
Placeholder
+
+ Click me! +
+
+ + diff --git a/test/ghostdriver-test/fixtures/common/scrolling_tests/target_page.html b/test/ghostdriver-test/fixtures/common/scrolling_tests/target_page.html new file mode 100644 index 0000000..0457a82 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/scrolling_tests/target_page.html @@ -0,0 +1,9 @@ + + + +Clicked Successfully! + + +

Clicked Successfully!

+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/selectPage.html b/test/ghostdriver-test/fixtures/common/selectPage.html new file mode 100644 index 0000000..1c785ce --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/selectPage.html @@ -0,0 +1,42 @@ + + + + +Multiple Selection test page + + + + + + + + + + + + + + + diff --git a/test/ghostdriver-test/fixtures/common/selectableItems.html b/test/ghostdriver-test/fixtures/common/selectableItems.html new file mode 100644 index 0000000..190b2ad --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/selectableItems.html @@ -0,0 +1,65 @@ + + + + + jQuery UI Selectable - Default functionality + + + + + + + + +
+ +
    +
  1. Item 1
  2. +
  3. Item 2
  4. +
  5. Item 3
  6. +
  7. Item 4
  8. +
  9. Item 5
  10. +
  11. Item 6
  12. +
  13. Item 7
  14. +
+ +
+ +
+ +

Enable a DOM element (or group of elements) to be selectable. Draw a box with your cursor to select items. Hold down the Ctrl key to make multiple non-adjacent selections.

+ + + +
+

no info

+
+ + +
+ + diff --git a/test/ghostdriver-test/fixtures/common/sessionCookie.html b/test/ghostdriver-test/fixtures/common/sessionCookie.html new file mode 100644 index 0000000..0ada24e --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/sessionCookie.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/test/ghostdriver-test/fixtures/common/sessionCookieDest.html b/test/ghostdriver-test/fixtures/common/sessionCookieDest.html new file mode 100644 index 0000000..f5e2ef2 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/sessionCookieDest.html @@ -0,0 +1,34 @@ + + + + + Session cookie destination + + + +This is the cookie destination page. + + diff --git a/test/ghostdriver-test/fixtures/common/simple.xml b/test/ghostdriver-test/fixtures/common/simple.xml new file mode 100644 index 0000000..01f4c87 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/simple.xml @@ -0,0 +1,5 @@ + + + baz + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/simpleTest.html b/test/ghostdriver-test/fixtures/common/simpleTest.html new file mode 100644 index 0000000..49bbc26 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/simpleTest.html @@ -0,0 +1,97 @@ + + + Hello WebDriver + + +

Heading

+ +

A single line of text

+ +
+

A div containing

+ More than one line of text
+ +
and block level elements
+
+ +An inline element + +

This line has lots + + of spaces. +

+ +

This line has a non-breaking space

+ +

This line has a   non-breaking space and spaces

+ +

These lines  
  have leading and trailing NBSPs  

+ +

This line has text within elements that are meant to be displayed + inline

+ +
+

before pre

+
   This section has a preformatted
+    text block    
+  split in four lines
+         
+

after pre

+
+ +

Some text

Some more text

+ +
Cheese

Some text

Some more text

and also

Brie
+ +
Hello, world
+ +
+ +
+ +
+

+ + +

+
+ + + + + +
Top level
+
+
Nested
+
+ + + + + +
beforeSpace afterSpace
+ + + + + +a link to an icon + +{a="b", c=1, d=true} +{a="\\b\\\"'\'"} + +            ​‌‍  ⁠ test            ​‌‍  ⁠  + + + diff --git a/test/ghostdriver-test/fixtures/common/slowLoadingAlert.html b/test/ghostdriver-test/fixtures/common/slowLoadingAlert.html new file mode 100644 index 0000000..a6216e3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/slowLoadingAlert.html @@ -0,0 +1,10 @@ + + + +slowLoadingAlert + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/slowLoadingResourcePage.html b/test/ghostdriver-test/fixtures/common/slowLoadingResourcePage.html new file mode 100644 index 0000000..e05f954 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/slowLoadingResourcePage.html @@ -0,0 +1,12 @@ + + + This page loads something slowly + + +

Simulate the situation where a web-bug or analytics script takes waaay + too long to respond. Normally these things are loaded in an iframe, which is + what we're doing here.

+ + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/slow_loading_iframes.html b/test/ghostdriver-test/fixtures/common/slow_loading_iframes.html new file mode 100644 index 0000000..d007248 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/slow_loading_iframes.html @@ -0,0 +1,14 @@ + + + + Page with slow loading iFrames + + + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/styledPage.html b/test/ghostdriver-test/fixtures/common/styledPage.html new file mode 100644 index 0000000..30810f0 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/styledPage.html @@ -0,0 +1,28 @@ + + + + Styled Page + + + + +
+ +
+ + +
+ +
+ +
Content
+ + + + + + diff --git a/test/ghostdriver-test/fixtures/common/svgPiechart.xhtml b/test/ghostdriver-test/fixtures/common/svgPiechart.xhtml new file mode 100644 index 0000000..bf060fd --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/svgPiechart.xhtml @@ -0,0 +1,81 @@ + + + + + Pie Chart Test + + + +
+ Some text for the chart. +
+
Nothing.
+
+ + + + + Test Chart + + + Apple + + Orange + + Banana + + Orange + + + + + + + + Example RotateScale - Rotate and scale transforms + + + + + + + + + + + + + + ABC (rotate) + + + + + + + + + + + + ABC (scale) + + + + +
WOrange
+
+
WOrange
+ + diff --git a/test/ghostdriver-test/fixtures/common/svgTest.svg b/test/ghostdriver-test/fixtures/common/svgTest.svg new file mode 100644 index 0000000..c6cc283 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/svgTest.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/ghostdriver-test/fixtures/common/tables.html b/test/ghostdriver-test/fixtures/common/tables.html new file mode 100644 index 0000000..a2bc957 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/tables.html @@ -0,0 +1,36 @@ + + + + Here be tables + + + + + + + + + +
HelloWorld(Cheese!)
+ + + + + +
some text +
some more text
+
+ + + + + + + + + +
Heading
Data 1Data 2
+ + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/transformable.xml b/test/ghostdriver-test/fixtures/common/transformable.xml new file mode 100644 index 0000000..0b7e7fd --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/transformable.xml @@ -0,0 +1,11 @@ + + + ]> + + +

Click the button.

+ + Go to another page + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/transformable.xsl b/test/ghostdriver-test/fixtures/common/transformable.xsl new file mode 100644 index 0000000..53db9fd --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/transformable.xsl @@ -0,0 +1,37 @@ + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/underscore.html b/test/ghostdriver-test/fixtures/common/underscore.html new file mode 100644 index 0000000..904a444 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/underscore.html @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/unicode_ltr.html b/test/ghostdriver-test/fixtures/common/unicode_ltr.html new file mode 100644 index 0000000..245acc7 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/unicode_ltr.html @@ -0,0 +1,8 @@ + + + + + +
‎Some notes‎
+ + diff --git a/test/ghostdriver-test/fixtures/common/upload.html b/test/ghostdriver-test/fixtures/common/upload.html new file mode 100644 index 0000000..aca398a --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/upload.html @@ -0,0 +1,45 @@ + + + + Upload Form + + + +
+
+ Enter a file to upload: +
+
+
+ + +
+ + diff --git a/test/ghostdriver-test/fixtures/common/userDefinedProperty.html b/test/ghostdriver-test/fixtures/common/userDefinedProperty.html new file mode 100644 index 0000000..2453e69 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/userDefinedProperty.html @@ -0,0 +1,8 @@ + + +
+ + + diff --git a/test/ghostdriver-test/fixtures/common/veryLargeCanvas.html b/test/ghostdriver-test/fixtures/common/veryLargeCanvas.html new file mode 100644 index 0000000..54a2aba --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/veryLargeCanvas.html @@ -0,0 +1,81 @@ + + + + Rectangles + + + + +
+
First Target
+
Second Target
+
Third Target
+
Fourth Target
+
Not a Target
+
Not a Target
+ + diff --git a/test/ghostdriver-test/fixtures/common/visibility-css.html b/test/ghostdriver-test/fixtures/common/visibility-css.html new file mode 100644 index 0000000..80cc649 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/visibility-css.html @@ -0,0 +1,21 @@ + + + + +Visibility test via CSS + +
+

Hello world. I like cheese.

+
+ + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/win32frameset.html b/test/ghostdriver-test/fixtures/common/win32frameset.html new file mode 100644 index 0000000..108b80f --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/win32frameset.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/common/window_switching_tests/page_with_frame.html b/test/ghostdriver-test/fixtures/common/window_switching_tests/page_with_frame.html new file mode 100644 index 0000000..b94733b --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/window_switching_tests/page_with_frame.html @@ -0,0 +1,12 @@ + + + + Test page for WindowSwitchingTest.testShouldFocusOnTheTopMostFrameAfterSwitchingToAWindow + + +

Open new window

+
+ +
+ + diff --git a/test/ghostdriver-test/fixtures/common/window_switching_tests/simple_page.html b/test/ghostdriver-test/fixtures/common/window_switching_tests/simple_page.html new file mode 100644 index 0000000..52c163c --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/window_switching_tests/simple_page.html @@ -0,0 +1,9 @@ + + + + Simple Page + + +
Simple page with simple test.
+ + diff --git a/test/ghostdriver-test/fixtures/common/xhtmlFormPage.xhtml b/test/ghostdriver-test/fixtures/common/xhtmlFormPage.xhtml new file mode 100644 index 0000000..aca53d3 --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/xhtmlFormPage.xhtml @@ -0,0 +1,17 @@ + + + + XHTML + + + +
+ + +
+ +

Here is some content that should not be in the previous p tag + + + diff --git a/test/ghostdriver-test/fixtures/common/xhtmlTest.html b/test/ghostdriver-test/fixtures/common/xhtmlTest.html new file mode 100644 index 0000000..d2f3a5d --- /dev/null +++ b/test/ghostdriver-test/fixtures/common/xhtmlTest.html @@ -0,0 +1,76 @@ + + + + XHTML Test Page + + +

+ + + +
+

XHTML Might Be The Future

+ +

If you'd like to go elsewhere then click me.

+ +

Alternatively, this goes to the same place.

+ +
+ +
+ + This link has the same text as another link: click me. +
+ +
Another div starts here.

+

An H2 title

+

Some more text

+
+ +
+ Foo +
    + +
+ +
+
+
+ + +
+
+
+ + I have width +
+
+
+
+ + +

+

+
Link=equalssign + +

Spaced out

+ + +
first_div
+
second_div
+ first_span + second_span +
+ +
I'm a parent +
I'm a child
+
+ +
Woo woo
+ + diff --git a/test/ghostdriver-test/fixtures/right_frame/0.html b/test/ghostdriver-test/fixtures/right_frame/0.html new file mode 100644 index 0000000..e6fa019 --- /dev/null +++ b/test/ghostdriver-test/fixtures/right_frame/0.html @@ -0,0 +1,38911 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/right_frame/1.html b/test/ghostdriver-test/fixtures/right_frame/1.html new file mode 100644 index 0000000..39b65dd --- /dev/null +++ b/test/ghostdriver-test/fixtures/right_frame/1.html @@ -0,0 +1,51 @@ + +WYSIWYG Editor Input Template + + + + + + + + + + + + +
  • foo.
  • bar
\ No newline at end of file diff --git a/test/ghostdriver-test/fixtures/right_frame/outside.html b/test/ghostdriver-test/fixtures/right_frame/outside.html new file mode 100644 index 0000000..b240224 --- /dev/null +++ b/test/ghostdriver-test/fixtures/right_frame/outside.html @@ -0,0 +1,414 @@ + + + Editing testDotAtEndDoesNotDelete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+ + + + +
+ +
+
+ +
+ + + + +
+ +
+
+
 
 
+ + + + + + + + + + + + + + + +
+
+
+
+ +
+ + +
+ + + + +
+
+ + + +
+
+
+

Document information

+

+ +

XWiki Syntax Help

Help on the XWiki Syntax

+
+ + +
+
+
+
+
+
This wiki is licensed under a Creative Commons 2.0 license
+
XWiki Enterprise 5.1-SNAPSHOT - Documentation
+
+
+ +
+
diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/1 b/test/ghostdriver-test/fixtures/testcase-issue_240/1 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/2 b/test/ghostdriver-test/fixtures/testcase-issue_240/2 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/3 b/test/ghostdriver-test/fixtures/testcase-issue_240/3 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/4 b/test/ghostdriver-test/fixtures/testcase-issue_240/4 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/5 b/test/ghostdriver-test/fixtures/testcase-issue_240/5 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/6 b/test/ghostdriver-test/fixtures/testcase-issue_240/6 new file mode 100644 index 0000000..e69de29 diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/test.html b/test/ghostdriver-test/fixtures/testcase-issue_240/test.html new file mode 100644 index 0000000..e5ebdb7 --- /dev/null +++ b/test/ghostdriver-test/fixtures/testcase-issue_240/test.html @@ -0,0 +1,97 @@ + + + + test + + + + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + 1 + 2 + 3 + 4 + 5 + 6 + + + + + diff --git a/test/ghostdriver-test/fixtures/testcase-issue_240/wb.rb b/test/ghostdriver-test/fixtures/testcase-issue_240/wb.rb new file mode 100644 index 0000000..d51106a --- /dev/null +++ b/test/ghostdriver-test/fixtures/testcase-issue_240/wb.rb @@ -0,0 +1,62 @@ +require 'json' +require 'shellwords' + +def c (method, url, data) + ret = `curl -s -X #{method} http://localhost:10000/#{url} --data-binary #{data.to_json.shellescape}` + return JSON.parse ret rescue ret +end + +def g (url, data={}) + c "GET", url, data +end + +def o (url, data) + c "POST", url, data +end + +def a (session, action, data = {}) + o "session/#{session}/#{action}", data +end + +def exec (session, js) + a(session, "execute", { script: "return #{js}", args: [] })["value"] +end + +def css (session, selector) + a(session, "elements", { using: 'css selector', value: selector })["value"].map { |x| x["ELEMENT"] } +end + +def click (session, element) + a(session, "element/#{element}/click") +end + +session_data = { + desiredCapabilities: { + browserName: "phantomjs", + platform: "ANY", + javascriptEnabled: true, + cssSelectorsEnabled: true, + takesScreenshot: true, + nativeEvents: false, + rotatable: false + } +} + +sessions = g("sessions", {})["value"] +if sessions.empty? + o("session", session_data) + sleep 0.5 +end +session = g("sessions", {})["value"][0]["id"] + +puts "Got session: #{session}" +a session, "url", { url: "http://localhost:8000/test.html" } +print "Going to do it now..." + + +css(session, "a").each do |i| + click session, i + # puts "Clicked on #{i}" +end + +puts exec session, "results" diff --git a/test/ghostdriver-test/java/build.gradle b/test/ghostdriver-test/java/build.gradle new file mode 100644 index 0000000..be11c3d --- /dev/null +++ b/test/ghostdriver-test/java/build.gradle @@ -0,0 +1,43 @@ +apply plugin: "java" +apply plugin: "idea" +apply plugin: "eclipse" + +task wrapper(type: Wrapper) { + gradleVersion = "1.10" + jarFile = "gradle/gradle-wrapper.jar" +} + +repositories { + mavenCentral() +} + +ext.commonsFileUploadVersion = "1.3" +ext.seleniumVersion = "2.39.0" +ext.junitVersion = "4.11" +ext.jettyVersion = "6.1.21" +ext.jsr305Version = "2.0.1" +ext.phantomjsdriverVersion = "1.1.0" + +dependencies { + ["selenium-java", "selenium-remote-driver", "selenium-server"].each { + testCompile "org.seleniumhq.selenium:$it:$seleniumVersion" + } + testCompile "com.google.code.findbugs:jsr305:$jsr305Version" + testCompile "junit:junit-dep:$junitVersion" + testCompile "org.mortbay.jetty:jetty:$jettyVersion" + testCompile "commons-fileupload:commons-fileupload:$commonsFileUploadVersion" + testCompile "com.github.detro.ghostdriver:phantomjsdriver:$phantomjsdriverVersion" +} + +tasks.withType(JavaExec) { + classpath = configurations.compile + sourceSets.test.output + args project.hasProperty("args") ? project.args.split("\\s") : [] +} + +test { + maxParallelForks = 3 + + afterTest { descriptor, result -> + logger.quiet(result.toString() + " for " + descriptor + " in " + descriptor.getParent()) + } +} diff --git a/test/ghostdriver-test/java/gradle/gradle-wrapper.properties b/test/ghostdriver-test/java/gradle/gradle-wrapper.properties new file mode 100644 index 0000000..dc3ff0d --- /dev/null +++ b/test/ghostdriver-test/java/gradle/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 30 09:41:35 GMT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip diff --git a/test/ghostdriver-test/java/gradlew b/test/ghostdriver-test/java/gradlew new file mode 100755 index 0000000..9e810c4 --- /dev/null +++ b/test/ghostdriver-test/java/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/test/ghostdriver-test/java/gradlew.bat b/test/ghostdriver-test/java/gradlew.bat new file mode 100644 index 0000000..661508d --- /dev/null +++ b/test/ghostdriver-test/java/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test/ghostdriver-test/java/settings.gradle b/test/ghostdriver-test/java/settings.gradle new file mode 100644 index 0000000..4fd8a72 --- /dev/null +++ b/test/ghostdriver-test/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "ghostdriver-test" diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTest.java new file mode 100644 index 0000000..76f9f45 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTest.java @@ -0,0 +1,163 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.phantomjs.PhantomJSDriver; +import org.openqa.selenium.phantomjs.PhantomJSDriverService; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +import java.io.FileReader; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Properties; + +/** + * Tests base class. + * Takes care of initialising the Remote WebDriver + */ +public abstract class BaseTest { + private WebDriver mDriver = null; + private boolean mAutoQuitDriver = true; + + private static final String CONFIG_FILE = "../config.ini"; + private static final String DRIVER_FIREFOX = "firefox"; + private static final String DRIVER_CHROME = "chrome"; + private static final String DRIVER_PHANTOMJS = "phantomjs"; + + protected static Properties sConfig; + protected static DesiredCapabilities sCaps; + + private static boolean isUrl(String urlString) { + try { + new URL(urlString); + return true; + } catch (MalformedURLException mue) { + return false; + } + } + + @BeforeClass + public static void configure() throws IOException { + // Read config file + sConfig = new Properties(); + sConfig.load(new FileReader(CONFIG_FILE)); + + // Prepare capabilities + sCaps = new DesiredCapabilities(); + sCaps.setJavascriptEnabled(true); + sCaps.setCapability("takesScreenshot", false); + + String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS); + + // Fetch PhantomJS-specific configuration parameters + if (driver.equals(DRIVER_PHANTOMJS)) { + // "phantomjs_exec_path" + if (sConfig.getProperty("phantomjs_exec_path") != null) { + sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY, sConfig.getProperty("phantomjs_exec_path")); + } else { + throw new IOException(String.format("Property '%s' not set!", PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY)); + } + // "phantomjs_driver_path" + if (sConfig.getProperty("phantomjs_driver_path") != null) { + System.out.println("Test will use an external GhostDriver"); + sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY, sConfig.getProperty("phantomjs_driver_path")); + } else { + System.out.println("Test will use PhantomJS internal GhostDriver"); + } + } + + // Disable "web-security", enable all possible "ssl-protocols" and "ignore-ssl-errors" for PhantomJSDriver +// sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, new String[] { +// "--web-security=false", +// "--ssl-protocol=any", +// "--ignore-ssl-errors=true" +// }); + ArrayList cliArgsCap = new ArrayList(); + cliArgsCap.add("--web-security=false"); + cliArgsCap.add("--ssl-protocol=any"); + cliArgsCap.add("--ignore-ssl-errors=true"); + sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, cliArgsCap); + + // Control LogLevel for GhostDriver, via CLI arguments + sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_CLI_ARGS, new String[] { + "--logLevel=" + (sConfig.getProperty("phantomjs_driver_loglevel") != null ? sConfig.getProperty("phantomjs_driver_loglevel") : "INFO") + }); + } + + @Before + public void prepareDriver() throws Exception + { + // Which driver to use? (default "phantomjs") + String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS); + + // Start appropriate Driver + if (isUrl(driver)) { + sCaps.setBrowserName("phantomjs"); + mDriver = new RemoteWebDriver(new URL(driver), sCaps); + } else if (driver.equals(DRIVER_FIREFOX)) { + mDriver = new FirefoxDriver(sCaps); + } else if (driver.equals(DRIVER_CHROME)) { + mDriver = new ChromeDriver(sCaps); + } else if (driver.equals(DRIVER_PHANTOMJS)) { + mDriver = new PhantomJSDriver(sCaps); + } + } + + protected WebDriver getDriver() { + return mDriver; + } + + protected void disableAutoQuitDriver() { + mAutoQuitDriver = false; + } + + protected void enableAutoQuitDriver() { + mAutoQuitDriver = true; + } + + protected boolean isAutoQuitDriverEnabled() { + return mAutoQuitDriver; + } + + @After + public void quitDriver() { + if (mAutoQuitDriver && mDriver != null) { + mDriver.quit(); + mDriver = null; + } + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTestWithServer.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTestWithServer.java new file mode 100644 index 0000000..37eded5 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/BaseTestWithServer.java @@ -0,0 +1,47 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.CallbackHttpServer; +import org.junit.After; +import org.junit.Before; + +abstract public class BaseTestWithServer extends BaseTest { + protected CallbackHttpServer server; + + @Before + public void startServer() throws Exception { + server = new CallbackHttpServer(); + server.start(); + } + + @After + public void stopServer() throws Exception { + server.stop(); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/CookieTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/CookieTest.java new file mode 100644 index 0000000..fa4ab34 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/CookieTest.java @@ -0,0 +1,317 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.EmptyPageHttpRequestCallback; +import ghostdriver.server.HttpRequestCallback; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.InvalidCookieDomainException; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; + +import static org.junit.Assert.*; + +public class CookieTest extends BaseTestWithServer { + private WebDriver driver; + + private final static HttpRequestCallback COOKIE_SETTING_CALLBACK = new EmptyPageHttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + super.call(req, res); + javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie("test", "test"); + cookie.setDomain(".localhost"); + cookie.setMaxAge(360); + + res.addCookie(cookie); + + cookie = new javax.servlet.http.Cookie("test2", "test2"); + cookie.setDomain(".localhost"); + res.addCookie(cookie); + } + }; + + private final static HttpRequestCallback EMPTY_CALLBACK = new EmptyPageHttpRequestCallback(); + + @Before + public void setup() { + driver = getDriver(); + } + + @After + public void cleanUp() { + driver.manage().deleteAllCookies(); + } + + private void goToPage(String path) { + driver.get(server.getBaseUrl() + path); + } + + private void goToPage() { + goToPage(""); + } + + private Cookie[] getCookies() { + return driver.manage().getCookies().toArray(new Cookie[]{}); + } + + @Test + public void gettingAllCookies() { + server.setHttpHandler("GET", COOKIE_SETTING_CALLBACK); + goToPage(); + Cookie[] cookies = getCookies(); + + assertEquals(2, cookies.length); + assertEquals("test", cookies[0].getName()); + assertEquals("test", cookies[0].getValue()); + assertEquals(".localhost", cookies[0].getDomain()); + assertEquals("/", cookies[0].getPath()); + assertTrue(cookies[0].getExpiry() != null); + assertEquals(false, cookies[0].isSecure()); + assertEquals("test2", cookies[1].getName()); + assertEquals("test2", cookies[1].getValue()); + assertEquals(".localhost", cookies[1].getDomain()); + assertEquals("/", cookies[1].getPath()); + assertEquals(false, cookies[1].isSecure()); + assertTrue(cookies[1].getExpiry() == null); + } + + @Test + public void gettingAllCookiesOnANonCookieSettingPage() { + server.setHttpHandler("GET", EMPTY_CALLBACK); + goToPage(); + assertEquals(0, getCookies().length); + } + + @Test + public void deletingAllCookies() { + server.setHttpHandler("GET", COOKIE_SETTING_CALLBACK); + goToPage(); + driver.manage().deleteAllCookies(); + assertEquals(0, getCookies().length); + } + + @Test + public void deletingOneCookie() { + server.setHttpHandler("GET", COOKIE_SETTING_CALLBACK); + goToPage(); + + driver.manage().deleteCookieNamed("test"); + + Cookie[] cookies = getCookies(); + + assertEquals(1, cookies.length); + assertEquals("test2", cookies[0].getName()); + } + + @Test + public void addingACookie() { + server.setHttpHandler("GET", EMPTY_CALLBACK); + goToPage(); + + driver.manage().addCookie(new Cookie("newCookie", "newValue")); + + Cookie[] cookies = getCookies(); + assertEquals(1, cookies.length); + assertEquals("newCookie", cookies[0].getName()); + assertEquals("newValue", cookies[0].getValue()); + assertEquals("localhost", cookies[0].getDomain()); + assertEquals("/", cookies[0].getPath()); + assertEquals(false, cookies[0].isSecure()); + } + + @Test + public void modifyingACookie() { + server.setHttpHandler("GET", COOKIE_SETTING_CALLBACK); + goToPage(); + + driver.manage().addCookie(new Cookie("test", "newValue", "localhost", "/", null, false)); + + Cookie[] cookies = getCookies(); + assertEquals(2, cookies.length); + assertEquals("test", cookies[0].getName()); + assertEquals("newValue", cookies[0].getValue()); + assertEquals(".localhost", cookies[0].getDomain()); + assertEquals("/", cookies[0].getPath()); + assertEquals(false, cookies[0].isSecure()); + + assertEquals("test2", cookies[1].getName()); + assertEquals("test2", cookies[1].getValue()); + assertEquals(".localhost", cookies[1].getDomain()); + assertEquals("/", cookies[1].getPath()); + assertEquals(false, cookies[1].isSecure()); + } + + @Test + public void shouldRetainCookieInfo() { + server.setHttpHandler("GET", EMPTY_CALLBACK); + goToPage(); + + // Added cookie (in a sub-path - allowed) + Cookie addedCookie = + new Cookie.Builder("fish", "cod") + .expiresOn(new Date(System.currentTimeMillis() + 100 * 1000)) //< now + 100sec + .path("/404") + .domain("localhost") + .build(); + driver.manage().addCookie(addedCookie); + + // Search cookie on the root-path and fail to find it + Cookie retrieved = driver.manage().getCookieNamed("fish"); + assertNull(retrieved); + + // Go to the "/404" sub-path (to find the cookie) + goToPage("404"); + retrieved = driver.manage().getCookieNamed("fish"); + assertNotNull(retrieved); + // Check that it all matches + assertEquals(addedCookie.getName(), retrieved.getName()); + assertEquals(addedCookie.getValue(), retrieved.getValue()); + assertEquals(addedCookie.getExpiry(), retrieved.getExpiry()); + assertEquals(addedCookie.isSecure(), retrieved.isSecure()); + assertEquals(addedCookie.getPath(), retrieved.getPath()); + assertTrue(retrieved.getDomain().contains(addedCookie.getDomain())); + } + + @Test(expected = InvalidCookieDomainException.class) + public void shouldNotAllowToCreateCookieOnDifferentDomain() { + goToPage(); + + // Added cookie (in a sub-path) + Cookie addedCookie = new Cookie.Builder("fish", "cod") + .expiresOn(new Date(System.currentTimeMillis() + 100 * 1000)) //< now + 100sec + .path("/404") + .domain("github.com") + .build(); + driver.manage().addCookie(addedCookie); + } + + @Test + public void shouldAllowToDeleteCookiesEvenIfNotSet() { + WebDriver d = getDriver(); + d.get("https://github.com/"); + + // Clear all cookies + assertTrue(d.manage().getCookies().size() > 0); + d.manage().deleteAllCookies(); + assertEquals(d.manage().getCookies().size(), 0); + + // All cookies deleted, call deleteAllCookies again. Should be a no-op. + d.manage().deleteAllCookies(); + d.manage().deleteCookieNamed("non_existing_cookie"); + assertEquals(d.manage().getCookies().size(), 0); + } + + @Test + public void shouldAllowToSetCookieThatIsAlreadyExpired() { + WebDriver d = getDriver(); + d.get("https://github.com/"); + + // Clear all cookies + assertTrue(d.manage().getCookies().size() > 0); + d.manage().deleteAllCookies(); + assertEquals(d.manage().getCookies().size(), 0); + + // Added cookie that expires in the past + Cookie addedCookie = new Cookie.Builder("expired", "yes") + .expiresOn(new Date(System.currentTimeMillis() - 1000)) //< now - 1 second + .build(); + d.manage().addCookie(addedCookie); + + Cookie cookie = d.manage().getCookieNamed("expired"); + assertNull(cookie); + } + + @Test(expected = Exception.class) + public void shouldThrowExceptionIfAddingCookieBeforeLoadingAnyUrl() { + // NOTE: At the time of writing, this test doesn't pass with FirefoxDriver. + // ChromeDriver is fine instead. + String xval = "123456789101112"; //< detro: I buy you a beer if you guess what am I quoting here + WebDriver d = getDriver(); + + // Set cookie, without opening any page: should throw an exception + d.manage().addCookie(new Cookie("x", xval)); + } + + @Test + public void shouldBeAbleToCreateCookieViaJavascriptOnGoogle() { + String ckey = "cookiekey"; + String cval = "cookieval"; + + WebDriver d = getDriver(); + d.get("http://www.google.com"); + JavascriptExecutor js = (JavascriptExecutor) d; + + // Of course, no cookie yet(!) + Cookie c = d.manage().getCookieNamed(ckey); + assertNull(c); + + // Attempt to create cookie on multiple Google domains + js.executeScript("javascript:(" + + "function() {" + + " cook = document.cookie;" + + " begin = cook.indexOf('"+ckey+"=');" + + " var val;" + + " if (begin !== -1) {" + + " var end = cook.indexOf(\";\",begin);" + + " if (end === -1)" + + " end=cook.length;" + + " val=cook.substring(begin+11,end);" + + " }" + + " val = ['"+cval+"'];" + + " if (val) {" + + " var d=Array('com','co.jp','ca','fr','de','co.uk','it','es','com.br');" + + " for (var i = 0; i < d.length; i++) {" + + " document.cookie = '"+ckey+"='+val+';path=/;domain=.google.'+d[i]+'; ';" + + " }" + + " }" + + "})();"); + c = d.manage().getCookieNamed(ckey); + assertNotNull(c); + assertEquals(cval, c.getValue()); + + // Set cookie as empty + js.executeScript("javascript:(" + + "function() {" + + " var d = Array('com','co.jp','ca','fr','de','co.uk','it','cn','es','com.br');" + + " for(var i = 0; i < d.length; i++) {" + + " document.cookie='"+ckey+"=;path=/;domain=.google.'+d[i]+'; ';" + + " }" + + "})();"); + c = d.manage().getCookieNamed(ckey); + assertNotNull(c); + assertEquals("", c.getValue()); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/DriverBasicTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/DriverBasicTest.java new file mode 100644 index 0000000..1190b79 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/DriverBasicTest.java @@ -0,0 +1,37 @@ +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.WebDriver; + +public class DriverBasicTest extends BaseTest { + @Test + public void useDriverButDontQuit() { + WebDriver d = getDriver(); + disableAutoQuitDriver(); + + d.get("http://www.google.com/"); + d.quit(); + } + +// @Test +// public void shouldSurviveExecutingManyTimesTheSameCommand() { +// WebDriver d = getDriver(); +// d.get("http://www.google.com"); +// for (int j = 0; j < 100; j++) { +// try { +// d.findElement(By.linkText(org.apache.commons.lang3.RandomStringUtils.randomAlphabetic(4))).isDisplayed(); +// } catch (NoSuchElementException nsee) { +// // swallow exceptions: we don't care about the result +// } +// } +// } +// +// @Test +// public void shouldSurviveExecutingManyTimesTheSameTest() throws Exception { +// for (int i = 0; i < 100; ++i) { +// prepareDriver(); +// shouldSurviveExecutingManyTimesTheSameCommand(); +// quitDriver(); +// } +// } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementFindingTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementFindingTest.java new file mode 100644 index 0000000..fca0fb7 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementFindingTest.java @@ -0,0 +1,261 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.*; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class ElementFindingTest extends BaseTestWithServer { + @Test + public void findChildElement() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("
" + + "" + + "" + + "
"); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + WebElement parent = d.findElement(By.id("y-masthead")); + + assertNotNull(parent.findElement(By.name("t"))); + } + + @Test + public void findChildElements() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("
" + + "" + + "" + + "
"); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + WebElement parent = d.findElement(By.id("y-masthead")); + + List children = parent.findElements(By.tagName("input")); + assertEquals(2, children.size()); + } + + @Test + public void findMultipleElements() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("
" + + "" + + "" + + "" + + "
"); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + assertEquals(3, d.findElements(By.tagName("input")).size()); + } + + @Test + public void findNoElementsMeetingCriteria() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + List els = d.findElements(By.name("noElementWithThisName")); + + assertEquals(0, els.size()); + } + + @Test + public void findNoChildElementsMeetingCriteria() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement parent = d.findElement(By.name("q")); + + List children = parent.findElements(By.tagName("input")); + + assertEquals(0, children.size()); + } + + @Test + public void findActiveElement() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement inputField = d.findElement(By.cssSelector("input[name='q']")); + WebElement active = d.switchTo().activeElement(); + + assertEquals(inputField.getTagName(), active.getTagName()); + assertEquals(inputField.getLocation(), active.getLocation()); + assertEquals(inputField.hashCode(), active.hashCode()); + assertEquals(inputField.getText(), active.getText()); + assertTrue(inputField.equals(active)); + } + + @Test(expected = NoSuchElementException.class) + public void failToFindNonExistentElement() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement inputField = d.findElement(By.cssSelector("input[name='idontexist']")); + } + + @Test(expected = InvalidSelectorException.class) + public void failFindElementForInvalidXPathLocator() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement inputField = d.findElement(By.xpath("this][isnot][valid")); + } + + @Test(expected = InvalidSelectorException.class) + public void failFindElementsForInvalidXPathLocator() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + List inputField = d.findElements(By.xpath("this][isnot][valid")); + } + + @Test + public void findElementWithImplicitWait() { + WebDriver d = getDriver(); + + d.get("about:blank"); + String injectLink = "document.body.innerHTML = \"add\""; + ((JavascriptExecutor)d).executeScript(injectLink); + WebElement add = d.findElement(By.id("add")); + + // DO NOT WAIT when looking for an element + d.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); + + // Add element + add.click(); + // Check element is not there yet + try { + d.findElement(By.id("testing1")); + throw new RuntimeException("expected NoSuchElementException"); + } catch (NoSuchElementException nse) { /* nothing to do */ } + + // DO WAIT 1 SECOND before giving up while looking for an element + d.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS); + // Add element + add.click(); + // Check element is there + assertNotNull(d.findElement(By.id("testing2"))); + + // DO WAIT 0.5 SECONDS before giving up while looking for an element + d.manage().timeouts().implicitlyWait(500, TimeUnit.MILLISECONDS); + // Add element + add.click(); + + // Check element is not there yet + try { + d.findElement(By.id("testing3")); + throw new RuntimeException("expected NoSuchElementException"); + } catch (NoSuchElementException nse) { /* nothing to do */ } + } + + @Test + public void findElementsWithImplicitWait() { + WebDriver d = getDriver(); + + d.get("about:blank"); + String injectLink = "document.body.innerHTML = \"add\""; + ((JavascriptExecutor)d).executeScript(injectLink); + WebElement add = d.findElement(By.id("add")); + + // DO NOT WAIT while looking for an element + d.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); + // Add element + add.click(); + // Check element is not there yet + assertEquals(0, d.findElements(By.id("testing1")).size()); + + // DO WAIT 3 SEC when looking for an element + d.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS); + // Add element + add.click(); + // Check element is there + assert(d.findElements(By.tagName("span")).size() >= 1 && d.findElements(By.tagName("span")).size() <= 2); + } + + @Test + public void findElementViaXpathLocator() { + // Define HTTP response for test + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + ServletOutputStream out = res.getOutputStream(); + out.println("" + + "" + + ""); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + WebElement loginButton = d.findElement(By.xpath("//button[contains(@class, 'login')]")); + assertNotNull(loginButton); + assertTrue(loginButton.getText().toLowerCase().contains("login")); + assertEquals("button", loginButton.getTagName().toLowerCase()); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementJQueryEventsTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementJQueryEventsTest.java new file mode 100644 index 0000000..23af93c --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementJQueryEventsTest.java @@ -0,0 +1,107 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; + +import static org.junit.Assert.assertTrue; + +@RunWith(value = Parameterized.class) +public class ElementJQueryEventsTest extends BaseTestWithServer { + + @Parameters(name = "jQuery Version: {0}") + public static Iterable data() { + return Arrays.asList(new Object[][]{ + {"2.0.3"}, {"2.0.2"}, {"2.0.1"}, {"2.0.0"}, + {"1.10.2"}, {"1.10.1"}, {"1.10.0"}, + {"1.9.1"}, {"1.9.0"}, + {"1.8.3"}, {"1.8.2"}, {"1.8.1"}, {"1.8.0"}, + {"1.7.2"}, //{"1.7.1"}, {"1.7.0"}, + {"1.6.4"}, //{"1.6.3"}, {"1.6.2"}, {"1.6.1"}, {"1.6.0"}, + {"1.5.2"}, //{"1.5.1"}, {"1.5.0"}, + {"1.4.4"}, //{"1.4.3"}, {"1.4.2"}, {"1.4.1"}, {"1.4.0"}, + {"1.3.2"}, //{"1.3.1"}, {"1.3.0"}, + {"1.2.6"}, //{"1.2.3"} + }); + } + + private String mJqueryVersion; + + public ElementJQueryEventsTest(String jQueryVersion) { + mJqueryVersion = jQueryVersion; + } + + @Test + public void shouldBeAbleToClickAndEventsBubbleUpUsingJquery() { + final String buttonId = "clickme"; + + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println( + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " click me\n" + + "\n" + + ""); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Click on the link inside the page + d.findElement(By.id(buttonId)).click(); + + // Check element was clicked as expected + assertTrue((Boolean)((JavascriptExecutor)d).executeScript("return clicked;")); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementMethodsTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementMethodsTest.java new file mode 100644 index 0000000..5908881 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementMethodsTest.java @@ -0,0 +1,243 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ElementMethodsTest extends BaseTestWithServer { + @Test + public void checkDisplayedOnGoogleSearchBox() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement el = d.findElement(By.cssSelector("input[name*='q']")); + + assertTrue(el.isDisplayed()); + + el = d.findElement(By.cssSelector("input[type='hidden']")); + + assertFalse(el.isDisplayed()); + } + + @Test + public void checkEnabledOnGoogleSearchBox() { + // TODO: Find a sample site that has hidden elements and use it to + // verify behavior of enabled and disabled elements. + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement el = d.findElement(By.cssSelector("input[name*='q']")); + + assertTrue(el.isEnabled()); + } + + @Test + public void checkClickOnAHREFCausesPageLoad() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement link = d.findElement(By.cssSelector("a[href=\"/intl/en/ads/\"]")); + link.click(); + + assertTrue(d.getTitle().contains("Ads")); + } + + @Test + public void checkClickOnINPUTSUBMITCausesPageLoad() { + WebDriver d = getDriver(); + + d.get("http://www.duckduckgo.com"); + WebElement textInput = d.findElement(By.cssSelector("#search_form_input_homepage")); + WebElement submitInput = d.findElement(By.cssSelector("#search_button_homepage")); + + + assertFalse(d.getTitle().contains("clicking")); + textInput.click(); + assertFalse(d.getTitle().contains("clicking")); + textInput.sendKeys("clicking"); + assertFalse(d.getTitle().contains("clicking")); + + submitInput.click(); + assertTrue(d.getTitle().contains("clicking")); + } + + @Test + public void SubmittingFormShouldFireOnSubmitForThatForm() { + WebDriver d = getDriver(); + + d.get(server.getBaseUrl() + "/common/javascriptPage.html"); + + WebElement formElement = d.findElement(By.id("submitListeningForm")); + formElement.submit(); + + WebDriverWait wait = new WebDriverWait(d, 30); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("result"), "form-onsubmit")); + + WebElement result = d.findElement(By.id("result")); + String text = result.getText(); + boolean conditionMet = text.contains("form-onsubmit"); + + assertTrue(conditionMet); + } + + @Test + public void shouldWaitForPossiblePageLoadOnlyWhenClickingOnSomeElement() { + WebDriver d = getDriver(); + + d.get("http://duckduckgo.com"); + WebElement inputTextEl = d.findElement(By.id("search_form_input_homepage")); + WebElement submitSearchDivWrapperEl = d.findElement(By.id("search_wrapper_homepage")); + WebElement submitSearchInputEl = d.findElement(By.id("search_button_homepage")); + + // Enter a query + inputTextEl.sendKeys("GhostDriver"); + + assertFalse(d.getTitle().contains("GhostDriver")); + // Ensure clicking on the Button DIV wrapper DOESN'T expect a pageload + submitSearchDivWrapperEl.click(); + assertFalse(d.getTitle().contains("GhostDriver")); + // Instead, clicking on the actual Input element DOES + submitSearchInputEl.click(); + assertTrue(d.getTitle().contains("GhostDriver")); + } + + @Test + public void shouldWaitForOnClickCallbackToFinishBeforeContinuing() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("\n" + + " Click Here"); + } + }); + + WebDriver d = getDriver(); + + d.get(server.getBaseUrl()); + d.findElement(By.xpath("html/body/a")).click(); + + assertTrue(d.getTitle().toLowerCase().contains("google")); + } + + @Test + public void shouldNotHandleCasesWhenAsyncJavascriptInitiatesAPageLoadFarInTheFuture() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("\n" + + " Click Here"); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Initiate timer that will finish with loading Google in the window + d.findElement(By.xpath("html/body/a")).click(); + + // "google.com" hasn't loaded yet at this stage + assertFalse(d.getTitle().toLowerCase().contains("google")); + } + + @Test + public void shouldHandleCasesWhereJavascriptCodeInitiatesPageLoadsThatFail() throws InterruptedException { + final String crazyUrl = "http://abcdefghilmnopqrstuvz.zvutsr"; + + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("\n" + + " Click Here"); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Click on the link to kickstart the javascript that will attempt to load a page that is supposed to fail + d.findElement(By.xpath("html/body/a")).click(); + + // The crazy URL should have not been loaded + assertTrue(!d.getCurrentUrl().equals(crazyUrl)); + } + + + @Test + public void shouldUsePageTimeoutToWaitForPageLoadOnInput() throws InterruptedException { + WebDriver d = getDriver(); + String inputString = "clicking"; + + d.get("http://www.duckduckgo.com"); + WebElement textInput = d.findElement(By.cssSelector("#search_form_input_homepage")); + + assertFalse(d.getTitle().contains(inputString)); + textInput.click(); + assertFalse(d.getTitle().contains(inputString)); + + // This input will ALSO submit the search form, causing a Page Load + textInput.sendKeys(inputString + "\n"); + + assertTrue(d.getTitle().contains(inputString)); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementQueryingTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementQueryingTest.java new file mode 100644 index 0000000..e415b56 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/ElementQueryingTest.java @@ -0,0 +1,148 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.*; +import org.openqa.selenium.internal.Locatable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ElementQueryingTest extends BaseTestWithServer { + @Test + public void checkAttributesOnGoogleSearchBox() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + WebElement el = d.findElement(By.cssSelector("input[name*='q']")); + + assertTrue(el.getAttribute("name").toLowerCase().contains("q")); + assertTrue(el.getAttribute("type").toLowerCase().contains("text")); + assertTrue(el.getAttribute("style").length() > 0); + assertTrue(el.getAttribute("type").length() > 0); + } + + @Test + public void checkLocationAndSizeOfBingSearchBox() { + WebDriver d = getDriver(); + d.manage().timeouts().pageLoadTimeout(20, TimeUnit.SECONDS); + + d.get("http://www.bing.com"); + WebElement searchBox = d.findElement(By.cssSelector("input[name*='q']")); + + assertTrue(searchBox.getCssValue("color").contains("rgb(0, 0, 0)") || searchBox.getCssValue("color").contains("rgba(0, 0, 0, 1)")); + assertEquals("", searchBox.getAttribute("value")); + assertEquals("input", searchBox.getTagName()); + assertEquals(true, searchBox.isEnabled()); + assertEquals(true, searchBox.isDisplayed()); + assertTrue(searchBox.getLocation().getX() >= 200); + assertTrue(searchBox.getLocation().getY() >= 100); + assertTrue(searchBox.getSize().getWidth() >= 350); + assertTrue(searchBox.getSize().getHeight() >= 20); + } + + @Test + public void scrollElementIntoView() { + WebDriver d = getDriver(); + + d.get("https://developer.mozilla.org/en/CSS/Attribute_selectors"); + WebElement aboutGoogleLink = d.findElement(By.partialLinkText("About MDN")); + Point locationBeforeScroll = aboutGoogleLink.getLocation(); + Point locationAfterScroll = ((Locatable) aboutGoogleLink).getCoordinates().inViewPort(); + + assertTrue(locationBeforeScroll.x >= locationAfterScroll.x); + assertTrue(locationBeforeScroll.y >= locationAfterScroll.y); + } + + @Test + public void getTextFromDifferentLocationsOfDOMTree() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("" + + "
\n" + + " \n" + + " \n" + + "

The Title of The Item

\n" + + "
\n" + + "
\n" + + "
\n" + + " (Loads of other stuff)\n" + + "
\n" + + "
" + + ""); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + assertEquals("The Title of The Item\n(Loads of other stuff)", d.findElement(By.className("item")).getText()); + assertEquals("The Title of The Item", d.findElement(By.className("item")).findElement(By.tagName("h1")).getText()); + assertEquals("The Title of The Item", d.findElement(By.className("item")).findElement(By.tagName("a")).getText()); + assertEquals("The Title of The Item", d.findElement(By.className("item")).findElement(By.className("item-title")).getText()); + } + + @Test(expected = InvalidElementStateException.class) + public void throwExceptionWhenInteractingWithInvisibleElement() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("" + + "" + + " \n" + + " test\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + + ""); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + WebElement visibleInput = d.findElement(By.id("visible")); + WebElement invisibleInput = d.findElement(By.id("invisible")); + + String textToType = "text to type"; + visibleInput.sendKeys(textToType); + assertEquals(textToType, visibleInput.getAttribute("value")); + + invisibleInput.sendKeys(textToType); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/FileUploadTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/FileUploadTest.java new file mode 100644 index 0000000..4a1ef62 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/FileUploadTest.java @@ -0,0 +1,171 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FileUploadTest extends BaseTestWithServer { + private static final String LOREM_IPSUM_TEXT = "lorem ipsum dolor sit amet"; + private static final String FILE_HTML = "
" + LOREM_IPSUM_TEXT + "
"; + + @Test + public void checkFileUploadCompletes() throws IOException { + WebDriver d = getDriver(); + + // Create the test file for uploading + File testFile = File.createTempFile("webdriver", "tmp"); + testFile.deleteOnExit(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(testFile.getAbsolutePath()), "utf-8")); + writer.write(FILE_HTML); + writer.close(); + + server.setHttpHandler("POST", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + if (ServletFileUpload.isMultipartContent(req) && req.getPathInfo().endsWith("/upload")) { + // Create a factory for disk-based file items + DiskFileItemFactory factory = new DiskFileItemFactory(1024, new File(System.getProperty("java.io.tmpdir"))); + + // Create a new file upload handler + ServletFileUpload upload = new ServletFileUpload(factory); + + // Parse the request + List items; + try { + items = upload.parseRequest(req); + } catch (FileUploadException fue) { + throw new IOException(fue); + } + + res.setHeader("Content-Type", "text/html; charset=UTF-8"); + InputStream is = items.get(0).getInputStream(); + OutputStream os = res.getOutputStream(); + IOUtils.copy(is, os); + + os.write("".getBytes()); + + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); + return; + } + + res.sendError(400); + } + }); + + // Upload the temp file + d.get(server.getBaseUrl() + "/common/upload.html"); + d.findElement(By.id("upload")).sendKeys(testFile.getAbsolutePath()); + d.findElement(By.id("go")).submit(); + + // Uploading files across a network may take a while, even if they're really small. + // Wait for the loading label to disappear. + WebDriverWait wait = new WebDriverWait(d, 10); + wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("upload_label"))); + + d.switchTo().frame("upload_target"); + + wait = new WebDriverWait(d, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.xpath("//body"), LOREM_IPSUM_TEXT)); + + // Navigate after file upload to verify callbacks are properly released. + d.get("http://www.google.com/"); + } + + @Test + public void checkFileUploadFailsIfFileDoesNotExist() throws InterruptedException { + WebDriver d = getDriver(); + + // Trying to upload a file that doesn't exist + d.get(server.getBaseUrl() + "/common/upload.html"); + d.findElement(By.id("upload")).sendKeys("file_that_does_not_exist.fake"); + d.findElement(By.id("go")).submit(); + + // Uploading files across a network may take a while, even if they're really small. + // Wait for a while and make sure the "upload_label" is still there: means that the file was not uploaded + Thread.sleep(1000); + assertTrue(d.findElement(By.id("upload_label")).isDisplayed()); + } + + @Test + public void checkUploadingTheSameFileMultipleTimes() throws IOException { + WebDriver d = getDriver(); + + File file = File.createTempFile("test", "txt"); + file.deleteOnExit(); + + d.get(server.getBaseUrl() + "/common/formPage.html"); + WebElement uploadElement = d.findElement(By.id("upload")); + uploadElement.sendKeys(file.getAbsolutePath()); + uploadElement.submit(); + + d.get(server.getBaseUrl() + "/common/formPage.html"); + uploadElement = d.findElement(By.id("upload")); + uploadElement.sendKeys(file.getAbsolutePath()); + uploadElement.submit(); + } + + @Test + public void checkOnChangeEventIsFiredOnFileUpload() throws IOException { + WebDriver d = getDriver(); + + d.get(server.getBaseUrl() + "/common/formPage.html"); + WebElement uploadElement = d.findElement(By.id("upload")); + WebElement result = d.findElement(By.id("fileResults")); + assertEquals("", result.getText()); + + File file = File.createTempFile("test", "txt"); + file.deleteOnExit(); + + uploadElement.sendKeys(file.getAbsolutePath()); + // Shift focus to something else because send key doesn't make the focus leave + d.findElement(By.id("id-name1")).click(); + + assertEquals("changed", result.getText()); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/FrameSwitchingTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/FrameSwitchingTest.java new file mode 100644 index 0000000..ee92613 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/FrameSwitchingTest.java @@ -0,0 +1,492 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import com.google.common.base.Predicate; +import ghostdriver.server.GetFixtureHttpRequestCallback; +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.*; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import javax.annotation.Nullable; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.junit.Assert.*; + +public class FrameSwitchingTest extends BaseTestWithServer { + + private String getCurrentFrameName(WebDriver driver) { + return (String)((JavascriptExecutor) driver).executeScript("return window.frameElement ? " + + "window.frameElement.name : " + + "'__MAIN_FRAME__';"); + } + + private boolean isAtTopWindow(WebDriver driver) { + return (Boolean)((JavascriptExecutor) driver).executeScript("return window == window.top"); + } + + @Test + public void switchToFrameByNumber() { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame(0); + assertEquals("packageFrame", getCurrentFrameName(d)); + d.switchTo().defaultContent(); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame(0); + assertEquals("packageFrame", getCurrentFrameName(d)); + } + + @Test + public void switchToFrameByName() { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame("packageFrame"); + assertEquals("packageFrame", getCurrentFrameName(d)); + d.switchTo().defaultContent(); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame("packageFrame"); + assertEquals("packageFrame", getCurrentFrameName(d)); + } + + @Test + public void switchToFrameByElement() { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame(d.findElement(By.name("packageFrame"))); + assertEquals("packageFrame", getCurrentFrameName(d)); + d.switchTo().defaultContent(); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + d.switchTo().frame(d.findElement(By.name("packageFrame"))); + assertEquals("packageFrame", getCurrentFrameName(d)); + } + + @Test(expected = NoSuchFrameException.class) + public void failToSwitchToFrameByName() throws Exception { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + d.switchTo().frame("unavailable frame"); + } + + @Test(expected = NoSuchElementException.class) + public void shouldBeAbleToClickInAFrame() throws InterruptedException { + WebDriver d = getDriver(); + + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + + d.switchTo().frame("classFrame"); + assertEquals("classFrame", getCurrentFrameName(d)); + + // This should cause a reload in the frame "classFrame" + d.findElement(By.linkText("HttpClient")).click(); + + // Wait for new content to load in the frame. + WebDriverWait wait = new WebDriverWait(d, 10); + wait.until(ExpectedConditions.titleContains("HttpClient")); + + // Frame should still be "classFrame" + assertEquals("classFrame", getCurrentFrameName(d)); + + // Check if a link "clearCookies()" is there where expected + assertEquals("clearCookies", d.findElement(By.linkText("clearCookies")).getText()); + + // Make sure it was really frame "classFrame" which was replaced: + // 1. move to the other frame "packageFrame" + d.switchTo().defaultContent().switchTo().frame("packageFrame"); + assertEquals("packageFrame", getCurrentFrameName(d)); + // 2. the link "clearCookies()" shouldn't be there anymore + d.findElement(By.linkText("clearCookies")); + } + + @Test(expected = NoSuchElementException.class) + public void shouldBeAbleToClickInAFrameAfterRunningJavaScript() throws InterruptedException { + WebDriver d = getDriver(); + + // Navigate to page and ensure we are on the Main Frame + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + assertTrue(isAtTopWindow(d)); + d.switchTo().defaultContent(); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + assertTrue(isAtTopWindow(d)); + + // Switch to a child frame + d.switchTo().frame("classFrame"); + assertEquals("classFrame", getCurrentFrameName(d)); + assertFalse(isAtTopWindow(d)); + + // Renavigate to the page, and check we are back on the Main Frame + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + assertTrue(isAtTopWindow(d)); + // Switch to a child frame + d.switchTo().frame("classFrame"); + assertEquals("classFrame", getCurrentFrameName(d)); + assertFalse(isAtTopWindow(d)); + + // This should cause a reload in the frame "classFrame" + d.findElement(By.linkText("HttpClient")).click(); + + // Wait for new content to load in the frame. + WebDriverWait wait = new WebDriverWait(d, 10); + wait.until(ExpectedConditions.titleContains("HttpClient")); + + // Frame should still be "classFrame" + assertEquals("classFrame", getCurrentFrameName(d)); + + // Check if a link "clearCookies()" is there where expected + assertEquals("clearCookies", d.findElement(By.linkText("clearCookies")).getText()); + + // Make sure it was really frame "classFrame" which was replaced: + // 1. move to the other frame "packageFrame" + d.switchTo().defaultContent().switchTo().frame("packageFrame"); + assertEquals("packageFrame", getCurrentFrameName(d)); + // 2. the link "clearCookies()" shouldn't be there anymore + d.findElement(By.linkText("clearCookies")); + } + + @Test + public void titleShouldReturnWindowTitle() { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + assertEquals("__MAIN_FRAME__", getCurrentFrameName(d)); + String topLevelTitle = d.getTitle(); + d.switchTo().frame("packageFrame"); + assertEquals("packageFrame", getCurrentFrameName(d)); + assertEquals(topLevelTitle, d.getTitle()); + d.switchTo().defaultContent(); + assertEquals(topLevelTitle, d.getTitle()); + } + + @Test + public void pageSourceShouldReturnSourceOfFocusedFrame() throws InterruptedException { + WebDriver d = getDriver(); + d.get("http://docs.wpm.neustar.biz/testscript-api/index.html"); + + // Compare source before and after the frame switch + String pageSource = d.getPageSource(); + d.switchTo().frame("classFrame"); + String framePageSource = d.getPageSource(); + assertFalse(pageSource.equals(framePageSource)); + + assertTrue("Page source was: " + framePageSource, framePageSource.contains("Interface Summary")); + } + + @Test + public void shouldSwitchBackToMainFrameIfLinkInFrameCausesTopFrameReload() throws Exception { + WebDriver d = getDriver(); + String expectedTitle = "Unique title"; + + class SpecialHttpRequestCallback extends GetFixtureHttpRequestCallback { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + if (req.getPathInfo().matches("^.*page/\\d+$")) { + int lastIndex = req.getPathInfo().lastIndexOf('/'); + String pageNumber = + (lastIndex == -1 ? "Unknown" : req.getPathInfo().substring(lastIndex + 1)); + String resBody = String.format("Page%s" + + "Page number %s" + + "

top" + + "", + pageNumber, pageNumber); + + res.getOutputStream().println(resBody); + } else { + super.call(req, res); + } + } + } + + server.setHttpHandler("GET", new SpecialHttpRequestCallback()); + + d.get(server.getBaseUrl() + "/common/frameset.html"); + assertEquals(expectedTitle, d.getTitle()); + + d.switchTo().frame(0); + d.findElement(By.linkText("top")).click(); + + // Wait for new content to load in the frame. + expectedTitle = "XHTML Test Page"; + WebDriverWait wait = new WebDriverWait(d, 10); + wait.until(ExpectedConditions.titleIs(expectedTitle)); + assertEquals(expectedTitle, d.getTitle()); + + WebElement element = d.findElement(By.id("amazing")); + assertNotNull(element); + } + + @Test + public void shouldSwitchBetweenNestedFrames() { + // Define HTTP response for test + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + String pathInfo = req.getPathInfo(); + ServletOutputStream out = res.getOutputStream(); + + // NOTE: the following pages are cut&paste from "Watir" test specs. + // @see https://github.com/watir/watirspec/tree/master/html/nested_frame.html + if (pathInfo.endsWith("nested_frame_1.html")) { + // nested frame 1 + out.println("frame 1"); + } else if (pathInfo.endsWith("nested_frame_2.html")) { + // nested frame 2 + out.println("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + ""); + } else if (pathInfo.endsWith("nested_frame_3.html")) { + // nested frame 3, nested inside frame 2 + out.println("\n" + + "\n" + + " \n" + + " this link should consume the page\n" + + " \n" + + ""); + } else if (pathInfo.endsWith("definition_lists.html")) { + // definition lists + out.println("\n" + + " \n" + + " definition_lists\n" + + " \n" + + ""); + } else { + // main page + out.println("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""); + } + } + }); + + // Launch Driver against the above defined server + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Switch to frame "#two" + d.switchTo().frame("two"); + // Switch further down into frame "#three" + d.switchTo().frame("three"); + // Click on the link in frame "#three" + d.findElement(By.id("four")).click(); + + // Expect page to have loaded and title to be set correctly + new WebDriverWait(d, 5).until(ExpectedConditions.titleIs("definition_lists")); + } + + @Test + public void shouldSwitchBetweenNestedFramesPickedViaWebElement() { + // Define HTTP response for test + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + String pathInfo = req.getPathInfo(); + ServletOutputStream out = res.getOutputStream(); + + // NOTE: the following pages are cut&paste from "Watir" test specs. + // @see https://github.com/watir/watirspec/tree/master/html/nested_frame.html + if (pathInfo.endsWith("nested_frame_1.html")) { + // nested frame 1 + out.println("frame 1"); + } else if (pathInfo.endsWith("nested_frame_2.html")) { + // nested frame 2 + out.println("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + ""); + } else if (pathInfo.endsWith("nested_frame_3.html")) { + // nested frame 3, nested inside frame 2 + out.println("\n" + + "\n" + + " \n" + + " this link should consume the page\n" + + " \n" + + ""); + } else if (pathInfo.endsWith("definition_lists.html")) { + // definition lists + out.println("\n" + + " \n" + + " definition_lists\n" + + " \n" + + ""); + } else { + // main page + out.println("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""); + } + } + }); + + // Launch Driver against the above defined server + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Switch to frame "#two" + d.switchTo().frame(d.findElement(By.id("two"))); + // Switch further down into frame "#three" + d.switchTo().frame(d.findElement(By.id("three"))); + // Click on the link in frame "#three" + d.findElement(By.id("four")).click(); + + // Expect page to have loaded and title to be set correctly + new WebDriverWait(d, 5).until(ExpectedConditions.titleIs("definition_lists")); + } + + @Test + public void shouldBeAbleToSwitchToIFrameThatHasNoNameNorId() { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("" + + "" + + " " + + "" + + ""); + } + }); + + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + WebElement el = d.findElement(By.tagName("iframe")); + d.switchTo().frame(el); + } + + @Test(expected = TimeoutException.class) + public void shouldTimeoutWhileChangingIframeSource() { + final String iFrameId = "iframeId"; + + // Define HTTP response for test + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + String pathInfo = req.getPathInfo(); + ServletOutputStream out = res.getOutputStream(); + + if (pathInfo.endsWith("iframe_content.html")) { + // nested frame 1 + out.println("iframe content"); + } else { + // main page + out.println("\n" + + "\n" + + " \n" + + " \n" + + "\n" + + ""); + } + } + }); + + // Launch Driver against the above defined server + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Switch to iframe + d.switchTo().frame(iFrameId); + assertEquals(0, d.findElements(By.id(iFrameId)).size()); + assertFalse(d.getPageSource().toLowerCase().contains("iframe content")); + + new WebDriverWait(d, 5).until(new Predicate() { + @Override + public boolean apply(@Nullable WebDriver driver) { + assertEquals(0, driver.findElements(By.id(iFrameId)).size()); + return (Boolean) ((JavascriptExecutor) driver).executeScript("return false;"); + } + }); + } + + @Test + public void shouldSwitchToTheRightFrame() { + WebDriver d = getDriver(); + + // Load "outside.html" and check it's the right one + d.get(server.getBaseUrl() + "right_frame/outside.html"); + assertTrue(d.getPageSource().contains("Editing testDotAtEndDoesNotDelete")); + assertEquals(2, d.findElements(By.tagName("iframe")).size()); + + // Find the iframe with class "gwt-RichTextArea" + WebElement iframeRichTextArea = d.findElement(By.className("gwt-RichTextArea")); + + // Switch to the iframe via WebElement and check it's the right one + d.switchTo().frame(iframeRichTextArea); + assertEquals(0, d.findElements(By.tagName("title")).size()); + assertFalse(d.getPageSource().contains("Editing testDotAtEndDoesNotDelete")); + assertEquals(0, d.findElements(By.tagName("iframe")).size()); + + // Switch back to the main frame and check it's the right one + d.switchTo().defaultContent(); + assertEquals(2, d.findElements(By.tagName("iframe")).size()); + + // Switch again to the iframe, this time via it's "frame number" (i.e. 0 to n) + d.switchTo().frame(0); + assertEquals(0, d.findElements(By.tagName("title")).size()); + assertFalse(d.getPageSource().contains("Editing testDotAtEndDoesNotDelete")); + assertEquals(0, d.findElements(By.tagName("iframe")).size()); + + // Switch back to the main frame and check it's the right one + d.switchTo().defaultContent(); + assertEquals(2, d.findElements(By.tagName("iframe")).size()); + + // Switch to the second frame via it's "frame number" + d.switchTo().frame(1); + assertEquals(1, d.findElements(By.tagName("title")).size()); + assertEquals(0, d.findElements(By.tagName("iframe")).size()); + assertTrue(d.getPageSource().contains("WYSIWYG Editor Input Template")); + + // Switch again to the main frame + d.switchTo().defaultContent(); + assertEquals(2, d.findElements(By.tagName("iframe")).size()); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/GoogleSearchTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/GoogleSearchTest.java new file mode 100644 index 0000000..20547a9 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/GoogleSearchTest.java @@ -0,0 +1,55 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import static org.junit.Assert.assertTrue; + +public class GoogleSearchTest extends BaseTest { + @Test + public void searchForCheese() { + String strToSearchFor = "Cheese!"; + WebDriver d = getDriver(); + + // Load Google.com + d.get(" http://www.google.com"); + // Locate the Search field on the Google page + WebElement element = d.findElement(By.name("q")); + // Type Cheese + element.sendKeys(strToSearchFor); + // Submit form + element.submit(); + + // Check results contains the term we searched for + assertTrue(d.getTitle().toLowerCase().contains(strToSearchFor.toLowerCase())); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/IsolatedSessionTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/IsolatedSessionTest.java new file mode 100644 index 0000000..2299dd2 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/IsolatedSessionTest.java @@ -0,0 +1,55 @@ +package ghostdriver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.WebDriver; + +import java.util.Set; + +import static org.junit.Assert.assertFalse; + +public class IsolatedSessionTest extends BaseTest { + // New Session Cookies will be stored in here + private String url = "http://www.google.com"; + private Set sessionCookies; + + @Before + public void createSession() throws Exception { + disableAutoQuitDriver(); + + WebDriver d = getDriver(); + d.get(url); + + // Grab set of session cookies + sessionCookies = d.manage().getCookies(); + + // Manually quit the current Driver and create a new one + d.quit(); + prepareDriver(); + } + + @Test + public void shouldCreateASeparateSessionWithEveryNewDriverInstance() { + WebDriver d = getDriver(); + d.get(url); + + // Grab NEW set of session cookies + Set newSessionCookies = d.manage().getCookies(); + + // No cookie of the new Session can be found in the cookies of the old Session + for (Cookie c : sessionCookies) { + assertFalse(newSessionCookies.contains(c)); + } + // No cookie of the old Session can be found in the cookies of the new Session + for (Cookie c : newSessionCookies) { + assertFalse(sessionCookies.contains(c)); + } + } + + @After + public void quitDriver() { + getDriver().quit(); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/LogTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/LogTest.java new file mode 100644 index 0000000..217d1f9 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/LogTest.java @@ -0,0 +1,90 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; + +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LogTest extends BaseTestWithServer { + @Test + public void shouldReturnListOfAvailableLogs() { + WebDriver d = getDriver(); + Set logTypes = d.manage().logs().getAvailableLogTypes(); + + if (d.getClass().getSimpleName().equals("PhantomJSDriver")) { + // GhostDriver only has 3 log types... + assertEquals(3, logTypes.size()); + // ... and "har" is one of them + assertTrue(logTypes.contains("har")); + } else { + assertTrue(logTypes.size() >= 2); + } + assertTrue(logTypes.contains("client")); + assertTrue(logTypes.contains("browser")); + + } + + @Test + public void shouldReturnLogTypeBrowser() { + WebDriver d = getDriver(); + d.get(server.getBaseUrl() + "/common/errors.html"); + // Throw 3 errors that are logged in the Browser's console + WebElement throwErrorButton = d.findElement(By.cssSelector("input[type='button']")); + throwErrorButton.click(); + throwErrorButton.click(); + throwErrorButton.click(); + + // Retrieve and count the errors + LogEntries logEntries = d.manage().logs().get("browser"); + assertEquals(3, logEntries.getAll().size()); + + for (LogEntry logEntry : logEntries) { + System.out.println(logEntry); + } + } + + @Test + public void shouldReturnLogTypeHar() { + WebDriver d = getDriver(); + d.get(server.getBaseUrl() + "/common/iframes.html"); + + LogEntries logEntries = d.manage().logs().get("har"); + for (LogEntry logEntry : logEntries) { + System.out.println(logEntry); + } + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/MouseCommandsTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/MouseCommandsTest.java new file mode 100644 index 0000000..02a3ccf --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/MouseCommandsTest.java @@ -0,0 +1,132 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MouseCommandsTest extends BaseTestWithServer { + @Test + public void move() { + WebDriver d = getDriver(); + Actions actionBuilder = new Actions(d); + + d.get("http://www.duckduckgo.com"); + + // Move mouse by x,y + actionBuilder.moveByOffset(100, 100).build().perform(); + // Move mouse on a given element + actionBuilder.moveToElement(d.findElement(By.id("logo_homepage"))).build().perform(); + // Move mouse on a given element, by x,y relative coordinates + actionBuilder.moveToElement(d.findElement(By.id("logo_homepage")), 50, 50).build().perform(); + } + + @Test + public void clickAndRightClick() { + WebDriver d = getDriver(); + Actions actionBuilder = new Actions(d); + + d.get("http://www.duckduckgo.com"); + + // Left click + actionBuilder.click().build().perform(); + // Right click + actionBuilder.contextClick(null).build().perform(); + // Right click on the logo (it will cause a "/moveto" before clicking + actionBuilder.contextClick(d.findElement(By.id("logo_homepage"))).build().perform(); + } + + @Test + public void doubleClick() { + WebDriver d = getDriver(); + Actions actionBuilder = new Actions(d); + + d.get("http://www.duckduckgo.com"); + + // Double click + actionBuilder.doubleClick().build().perform(); + // Double click on the logo + actionBuilder.doubleClick(d.findElement(By.id("logo_homepage"))).build().perform(); + } + + @Test + public void clickAndHold() { + WebDriver d = getDriver(); + Actions actionBuilder = new Actions(d); + + d.get("http://www.duckduckgo.com"); + + // Hold, then release + actionBuilder.clickAndHold().build().perform(); + actionBuilder.release(); + // Hold on the logo, then release + actionBuilder.clickAndHold(d.findElement(By.id("logo_homepage"))).build().perform(); + actionBuilder.release(); + } + + @Test + public void handleClickWhenOnClickInlineCodeFails() { + // Define HTTP response for test + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("" + + "" + + "\n" + + "" + + "" + + "\n" + + "Click me" + + "" + + ""); + } + }); + + // Navigate to local server + WebDriver d = getDriver(); + d.navigate().to(server.getBaseUrl()); + + WebElement el = d.findElement(By.linkText("Click me")); + el.click(); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/NavigationTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/NavigationTest.java new file mode 100644 index 0000000..6360e36 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/NavigationTest.java @@ -0,0 +1,90 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.phantomjs.PhantomJSDriverService; + +import static org.junit.Assert.assertTrue; + +public class NavigationTest extends BaseTest { + @BeforeClass + public static void setUserAgentForPhantomJSDriver() { + // Setting a generic Chrome UA to bypass some UA spoofing + sCaps.setCapability( + PhantomJSDriverService.PHANTOMJS_PAGE_SETTINGS_PREFIX + "userAgent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11" + ); + } + + @Test + public void navigateAroundMDN() { + WebDriver d = getDriver(); + + d.get("https://developer.mozilla.org/en-US/"); + assertTrue(d.getTitle().toLowerCase().contains("Mozilla".toLowerCase())); + d.navigate().to("https://developer.mozilla.org/en/HTML/HTML5"); + assertTrue(d.getTitle().toLowerCase().contains("HTML5".toLowerCase())); + d.navigate().refresh(); + assertTrue(d.getTitle().toLowerCase().contains("HTML5".toLowerCase())); + d.navigate().back(); + assertTrue(d.getTitle().toLowerCase().contains("Mozilla".toLowerCase())); + d.navigate().forward(); + assertTrue(d.getTitle().toLowerCase().contains("HTML5".toLowerCase())); + } + + @Test + public void navigateBackWithNoHistory() throws Exception { + // Fresh Driver (every test gets one) + WebDriver d = getDriver(); + + // Navigate back and forward: should be a no-op, given we haven't loaded anything yet + d.navigate().back(); + d.navigate().forward(); + + // Make sure explicit navigation still works. + d.get("http://google.com"); + } + + @Test + public void navigateToGoogleAdwords() { + WebDriver d = getDriver(); + d.get("http://adwords.google.com"); + assertTrue(d.getCurrentUrl().contains("google.com")); + } + + @Test + public void navigateToNameJet() { + // NOTE: This passes only when the User Agent is NOT PhantomJS {@see setUserAgentForPhantomJSDriver} + // method above. + WebDriver d = getDriver(); + d.navigate().to("http://www.namejet.com/"); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java new file mode 100755 index 0000000..56c1eee --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/PhantomJSCommandTest.java @@ -0,0 +1,49 @@ +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.phantomjs.PhantomJSDriver; + +import static junit.framework.TestCase.assertEquals; + +public class PhantomJSCommandTest extends BaseTest { + @Test + public void executePhantomJS() { + WebDriver d = getDriver(); + if (!(d instanceof PhantomJSDriver)) { + // Skip this test if not using PhantomJS. + // The command under test is only available when using PhantomJS + return; + } + + PhantomJSDriver phantom = (PhantomJSDriver)d; + + // Do we get results back? + Object result = phantom.executePhantomJS("return 1 + 1"); + assertEquals(new Long(2), (Long)result); + + // Can we read arguments? + result = phantom.executePhantomJS("return arguments[0] + arguments[0]", new Long(1)); + assertEquals(new Long(2), (Long)result); + + // Can we override some browser JavaScript functions in the page context? + result = phantom.executePhantomJS("var page = this;" + + "page.onInitialized = function () { " + + "page.evaluate(function () { " + + "Math.random = function() { return 42 / 100 } " + + "})" + + "}"); + + phantom.get("http://ariya.github.com/js/random/"); + + WebElement numbers = phantom.findElement(By.id("numbers")); + boolean foundAtLeastOne = false; + for(String number : numbers.getText().split(" ")) { + foundAtLeastOne = true; + assertEquals("42", number); + } + assert(foundAtLeastOne); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/ScriptExecutionTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/ScriptExecutionTest.java new file mode 100644 index 0000000..2ecd30b --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/ScriptExecutionTest.java @@ -0,0 +1,136 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.Ignore; +import org.junit.Test; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class ScriptExecutionTest extends BaseTest { + @Test + public void findGoogleInputFieldInjectingJavascript() { + WebDriver d = getDriver(); + d.get("http://www.google.com"); + WebElement e = (WebElement)((JavascriptExecutor) d).executeScript( + "return document.querySelector(\"[name='\"+arguments[0]+\"']\");", + "q"); + assertNotNull(e); + assertEquals("input", e.getTagName().toLowerCase()); + } + + @Test + public void setTimeoutAsynchronously() { + WebDriver d = getDriver(); + d.get("http://www.google.com"); + String res = (String)((JavascriptExecutor) d).executeAsyncScript( + "window.setTimeout(arguments[arguments.length - 1], arguments[0], 'done');", + 1000); + assertEquals("done", res); + } + + @Test + public void shouldBeAbleToPassMultipleArgumentsToAsyncScripts() { + WebDriver d = getDriver(); + d.manage().timeouts().setScriptTimeout(0, TimeUnit.MILLISECONDS); + d.get("http://www.google.com/"); + Number result = (Number) ((JavascriptExecutor) d).executeAsyncScript( + "arguments[arguments.length - 1](arguments[0] + arguments[1]);", + 1, + 2); + assertEquals(3, result.intValue()); + + // Verify that a future navigation does not cause the driver to have problems. + d.get("http://www.google.com/"); + } + + @Test + public void shouldBeAbleToExecuteMultipleAsyncScriptsSequentially() { + WebDriver d = getDriver(); + d.manage().timeouts().setScriptTimeout(0, TimeUnit.MILLISECONDS); + d.get("http://www.google.com/"); + Number numericResult = (Number) ((JavascriptExecutor) d).executeAsyncScript( + "arguments[arguments.length - 1](123);"); + assertEquals(123, numericResult.intValue()); + String stringResult = (String) ((JavascriptExecutor) d).executeAsyncScript( + "arguments[arguments.length - 1]('abc');"); + assertEquals("abc", stringResult); + } + + @Ignore("Known issue #140 - see https://github.com/detro/ghostdriver/issues/140)") + @Test + public void shouldBeAbleToExecuteMultipleAsyncScriptsSequentiallyWithNavigation() { + // NOTE: This test is supposed to fail! + // It's a reminder that there is some internal issue in PhantomJS still to address. + + WebDriver d = getDriver(); + d.manage().timeouts().setScriptTimeout(0, TimeUnit.MILLISECONDS); + + d.get("http://www.google.com/"); + Number numericResult = (Number) ((JavascriptExecutor) d).executeAsyncScript( + "arguments[arguments.length - 1](123);"); + assertEquals(123, numericResult.intValue()); + + d.get("http://www.google.com/"); + String stringResult = (String) ((JavascriptExecutor) d).executeAsyncScript( + "arguments[arguments.length - 1]('abc');"); + assertEquals("abc", stringResult); + + // Verify that a future navigation does not cause the driver to have problems. + d.get("http://www.google.com/"); + } + + @Ignore("Known issue #140 - see https://github.com/detro/ghostdriver/issues/140)") + @Test + public void executeAsyncScriptMultipleTimesWithoutCrashing() { + // NOTE: This test is supposed to fail! + // It's a reminder that there is some internal issue in PhantomJS still to address. + + WebDriver d = getDriver(); + + String hello = null; + try { + hello = URLEncoder.encode("

hello

", "UTF-8"); + } catch (UnsupportedEncodingException uee) { + fail(); + } + + for (int i = 1; i < 5; ++i) { + d.get("data:text/html;content-type=utf-8,"+hello); + String h = (String)((JavascriptExecutor) d).executeAsyncScript("arguments[arguments.length - 1]('hello')"); + assertEquals("hello", h); + } + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/SessionBasicTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/SessionBasicTest.java new file mode 100644 index 0000000..5ec1645 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/SessionBasicTest.java @@ -0,0 +1,59 @@ +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchWindowException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.SessionNotFoundException; + +import java.net.MalformedURLException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class SessionBasicTest extends BaseTest { + + @Test(expected = SessionNotFoundException.class) + public void quitShouldTerminatePhantomJSProcess() throws MalformedURLException { + // Get Driver Instance + WebDriver d = getDriver(); + d.navigate().to("about:blank"); + + // Quit the driver, that will cause the process to close + d.quit(); + + // Throws "SessionNotFoundException", because no process is actually left to respond + d.getWindowHandle(); + } + + @Test(expected = NoSuchWindowException.class) + public void closeShouldNotTerminatePhantomJSProcess() throws MalformedURLException { + // By default, 1 window is created when Driver is launched + WebDriver d = getDriver(); + assertEquals(1, d.getWindowHandles().size()); + + // Check the number of windows + d.navigate().to("about:blank"); + assertEquals(1, d.getWindowHandles().size()); + + // Create a new window + ((JavascriptExecutor) d).executeScript("window.open('http://www.google.com','google');"); + assertEquals(2, d.getWindowHandles().size()); + + // Close 1 window and check that 1 is left + d.close(); + assertEquals(1, d.getWindowHandles().size()); + + // Switch to that window + d.switchTo().window("google"); + assertNotNull(d.getWindowHandle()); + + // Close the remaining window and check now there are no windows available + d.close(); + assertEquals(0, d.getWindowHandles().size()); + + // This should throw a "NoSuchWindowException": the Driver is still running, but no Session/Window are left + d.getWindowHandle(); + } + +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/TimeoutSettingTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/TimeoutSettingTest.java new file mode 100644 index 0000000..4ffa97b --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/TimeoutSettingTest.java @@ -0,0 +1,43 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.WebDriver; + +import java.util.concurrent.TimeUnit; + +public class TimeoutSettingTest extends BaseTest { + @Test + public void navigateAroundMDN() { + WebDriver d = getDriver(); + d.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + d.manage().timeouts().pageLoadTimeout(20, TimeUnit.SECONDS); + d.manage().timeouts().setScriptTimeout(5, TimeUnit.SECONDS); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowHandlesTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowHandlesTest.java new file mode 100644 index 0000000..2338eb4 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowHandlesTest.java @@ -0,0 +1,103 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import ghostdriver.server.HttpRequestCallback; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.*; + +public class WindowHandlesTest extends BaseTestWithServer { + @Test + public void enumerateWindowHandles() { + WebDriver d = getDriver(); + + // Didn't open any page yet: no Window Handles yet + Set whandles = d.getWindowHandles(); + assertEquals(whandles.size(), 1); + + // Open Google and count the Window Handles: there should be at least 1 + d.get("http://www.google.com"); + whandles = d.getWindowHandles(); + assertEquals(whandles.size(), 1); + } + + @Test + public void enumerateWindowHandle() { + WebDriver d = getDriver(); + + // Didn't open any page yet: no Window Handles yet + String whandle = d.getWindowHandle(); + assertFalse(whandle.isEmpty()); + } + + @Test + public void openPopupAndGetCurrentUrl() throws InterruptedException { + server.setHttpHandler("GET", new HttpRequestCallback() { + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println("" + + "" + + "\n" + + "\n" + + " \n" + + " Link to popup" + + " \n" + + ""); + } + }); + + // Load page + WebDriver d = getDriver(); + d.get(server.getBaseUrl()); + + // Click on link that will cause popup to be created + d.findElement(By.xpath("//a")).click(); + // Switch to new popup + String popupHandle = (String)d.getWindowHandles().toArray()[1]; + d.switchTo().window(popupHandle); + + assertTrue(d.getTitle().contains("Japan")); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSizingAndPositioningTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSizingAndPositioningTest.java new file mode 100644 index 0000000..4d0c04d --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSizingAndPositioningTest.java @@ -0,0 +1,78 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import org.junit.Test; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebDriver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class WindowSizingAndPositioningTest extends BaseTest { + @Test + public void manipulateWindowSize() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + assertTrue(d.manage().window().getSize().width > 100); + assertTrue(d.manage().window().getSize().height > 100); + + d.manage().window().setSize(new Dimension(1024, 768)); + assertEquals(d.manage().window().getSize().width, 1024); + assertEquals(d.manage().window().getSize().height, 768); + } + + @Test + public void manipulateWindowPosition() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + assertTrue(d.manage().window().getPosition().x >= 0); + assertTrue(d.manage().window().getPosition().y >= 0); + + d.manage().window().setPosition(new Point(0, 0)); + assertTrue(d.manage().window().getPosition().x == 0); + assertTrue(d.manage().window().getPosition().y == 0); + } + + @Test + public void manipulateWindowMaximize() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + + Dimension sizeBefore = d.manage().window().getSize(); + d.manage().window().maximize(); + Dimension sizeAfter = d.manage().window().getSize(); + + assertTrue(sizeBefore.width <= sizeAfter.width); + assertTrue(sizeBefore.height <= sizeAfter.height); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSwitchingTest.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSwitchingTest.java new file mode 100644 index 0000000..8fef265 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/WindowSwitchingTest.java @@ -0,0 +1,220 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver; + +import com.google.common.base.Function; +import org.junit.Test; +import org.openqa.selenium.*; +import org.openqa.selenium.support.ui.WebDriverWait; + +import javax.annotation.Nullable; + +import static org.junit.Assert.*; + +public class WindowSwitchingTest extends BaseTestWithServer { + @Test + public void switchBetween3WindowsThenDeleteSecondOne() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + String googleWH = d.getWindowHandle(); + assertEquals(d.getWindowHandles().size(), 1); + + // Open a new window and make sure the window handle is different + ((JavascriptExecutor) d).executeScript("window.open('http://www.yahoo.com', 'yahoo')"); + assertEquals(d.getWindowHandles().size(), 2); + String yahooWH = (String) d.getWindowHandles().toArray()[1]; + assertTrue(!yahooWH.equals(googleWH)); + + // Switch to the yahoo window and check that the current window handle has changed + d.switchTo().window(yahooWH); + assertEquals(d.getWindowHandle(), yahooWH); + + // Open a new window and make sure the window handle is different + ((JavascriptExecutor) d).executeScript("window.open('http://www.bing.com', 'bing')"); + assertEquals(d.getWindowHandles().size(), 3); + String bingWH = (String) d.getWindowHandles().toArray()[2]; + assertTrue(!bingWH.equals(googleWH)); + assertTrue(!bingWH.equals(yahooWH)); + + // Close yahoo window + d.close(); + + // Switch to google window and notice that only google and bing are left + d.switchTo().window(googleWH); + assertEquals(d.getWindowHandles().size(), 2); + assertTrue(d.getWindowHandles().contains(googleWH)); + assertTrue(d.getWindowHandles().contains(bingWH)); + } + + @Test(expected = NoSuchWindowException.class) + public void switchBetween3WindowsThenDeleteFirstOne() { + WebDriver d = getDriver(); + + d.get("http://www.google.com"); + String googleWH = d.getWindowHandle(); + assertEquals(d.getWindowHandles().size(), 1); + + // Open a new window and make sure the window handle is different + ((JavascriptExecutor) d).executeScript("window.open('http://www.yahoo.com', 'yahoo')"); + assertEquals(d.getWindowHandles().size(), 2); + String yahooWH = (String) d.getWindowHandles().toArray()[1]; + assertTrue(!yahooWH.equals(googleWH)); + + // Switch to the yahoo window and check that the current window handle has changed + d.switchTo().window(yahooWH); + assertEquals(d.getWindowHandle(), yahooWH); + + // Open a new window and make sure the window handle is different + ((JavascriptExecutor) d).executeScript("window.open('http://www.bing.com', 'bing')"); + assertEquals(d.getWindowHandles().size(), 3); + String bingWH = (String) d.getWindowHandles().toArray()[2]; + assertTrue(!bingWH.equals(googleWH)); + assertTrue(!bingWH.equals(yahooWH)); + + // Switch to google window and close it + d.switchTo().window(googleWH); + d.close(); + + // Notice that yahoo and bing are the only left + assertEquals(d.getWindowHandles().size(), 2); + assertTrue(d.getWindowHandles().contains(yahooWH)); + assertTrue(d.getWindowHandles().contains(bingWH)); + + // Try getting the title of the, now closed, google window and cause an Exception + d.getTitle(); + } + + @Test + public void switchToSameWindowViaHandle() { + WebDriver d = getDriver(); + d.navigate().to(server.getBaseUrl() + "/common/frameset.html"); + + // Get handle of the main html page + String windowHandle = d.getWindowHandle(); + + // Verify that the element can be retrieved from the main page: + WebElement e = d.findElement(By.tagName("frameset")); + assertNotNull(e); + + // Switch to the frame. + d.switchTo().frame(0); + e = null; + try { + e = d.findElement(By.tagName("frameset")); + } catch (NoSuchElementException ex) { + // swallow the exception + } + assertNull(e); + + // Switch back to the main page using the original window handle: + d.switchTo().window(windowHandle); + + // This then throws an element not found exception.. the main page was not selected. + e = d.findElement(By.tagName("frameset")); + assertNotNull(e); + } + + @Test + public void shouldBeAbleToClickALinkThatClosesAWindow() throws Exception { + final WebDriver d = getDriver(); + d.get(server.getBaseUrl() + "/common/javascriptPage.html"); + + String handle = d.getWindowHandle(); + d.findElement(By.id("new_window")).click(); + + // Wait until we can switch to the new window + WebDriverWait waiter = new WebDriverWait(d, 10); + waiter.until(new Function() { + @Override + public Object apply(@Nullable WebDriver input) { + try { + d.switchTo().window("close_me"); + return true; + } catch (Exception e) { + return false; + } + } + }); + assertEquals(0, d.findElements(By.id("new_window")).size()); + + // Click on the "close" link. + // NOTE : This will cause the window currently in focus to close + d.findElement(By.id("close")).click(); + + d.switchTo().window(handle); + assertNotNull(d.findElement(By.id("new_window"))); + + // NOTE: If we haven't seen an exception or hung the test has passed + } + + @Test + public void shouldNotBeAbleToSwitchBackToInitialWindowUsingEmptyWindowNameParameter() { + final WebDriver d = getDriver(); + + d.get(server.getBaseUrl() + "/common/xhtmlTest.html"); + + // Store the first window handle + String initialWindowHandle = d.getWindowHandle(); + + // Ensure we are where we think we are, then click on "windowOne" to open another window + assertEquals(1, d.findElements(By.name("windowOne")).size()); + d.findElement(By.name("windowOne")).click(); + + // Wait until we can switch to the new window + WebDriverWait waiter = new WebDriverWait(d, 10); + waiter.until(new Function() { + @Override + public Object apply(@Nullable WebDriver input) { + try { + d.switchTo().window("result"); + return true; + } catch (Exception e) { + return false; + } + } + }); + // Check we are on the new window + assertEquals(1, d.findElements(By.id("greeting")).size()); + + // Switch to the first screen that has "window.name = ''". Usually, the first window of the Session. + d.switchTo().window(""); + // Close the window + d.close(); + + try { + // This second call to switch to window with empty string should fail. + // NOTE: I can't use "@Test(expected..." because the first call might throw the same exception + // and we won't be able to distinguish. + d.switchTo().window(""); + fail(); + } catch (NoSuchWindowException nswe) { + // If we are here, all is happening as expected + } + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackHttpServer.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackHttpServer.java new file mode 100644 index 0000000..de57db4 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackHttpServer.java @@ -0,0 +1,79 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver.server; + +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.Context; +import org.mortbay.jetty.servlet.ServletHolder; + +import java.util.HashMap; +import java.util.Map; + +import static java.lang.String.format; + +public class CallbackHttpServer { + + protected Server server; + protected final Map httpReqCallbacks = new HashMap(); + + public CallbackHttpServer() { + // Default HTTP GET request callback: returns files in "test/fixture" + setHttpHandler("GET", new GetFixtureHttpRequestCallback()); + } + + public HttpRequestCallback getHttpHandler(String httpMethod) { + return httpReqCallbacks.get(httpMethod.toUpperCase()); + } + + public void setHttpHandler(String httpMethod, HttpRequestCallback getHandler) { + httpReqCallbacks.put(httpMethod.toUpperCase(), getHandler); + } + + public void start() throws Exception { + server = new Server(0); + Context context = new Context(server, "/"); + addServlets(context); + server.start(); + } + + public void stop() throws Exception { + server.stop(); + } + + protected void addServlets(Context context) { + context.addServlet(new ServletHolder(new CallbackServlet(this)), "/*"); + } + + public int getPort() { + return server.getConnectors()[0].getLocalPort(); + } + + public String getBaseUrl() { + return format("http://localhost:%d/", getPort()); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackServlet.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackServlet.java new file mode 100644 index 0000000..8fcec82 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/CallbackServlet.java @@ -0,0 +1,58 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver.server; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CallbackServlet extends HttpServlet { + private CallbackHttpServer server; + + CallbackServlet(CallbackHttpServer server) { + this.server = server; + } + + protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { + if (server.getHttpHandler("GET") != null) { + server.getHttpHandler("GET").call(req, res); + } else { + super.doGet(req, res); + } + } + + protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { + if (server.getHttpHandler("POST") != null) { + server.getHttpHandler("POST").call(req, res); + } else { + super.doPost(req, res); + } + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/server/EmptyPageHttpRequestCallback.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/EmptyPageHttpRequestCallback.java new file mode 100644 index 0000000..a3555d0 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/EmptyPageHttpRequestCallback.java @@ -0,0 +1,40 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class EmptyPageHttpRequestCallback implements HttpRequestCallback { + + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + res.getOutputStream().println(""); + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/server/GetFixtureHttpRequestCallback.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/GetFixtureHttpRequestCallback.java new file mode 100644 index 0000000..29cc9f4 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/GetFixtureHttpRequestCallback.java @@ -0,0 +1,64 @@ +package ghostdriver.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.logging.Logger; + +public class GetFixtureHttpRequestCallback implements HttpRequestCallback { + + private static final Logger LOG = Logger.getLogger(GetFixtureHttpRequestCallback.class.getName()); + + private static final String FIXTURE_PATH = "../fixtures"; + + @Override + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException { + // Construct path to the file + Path filePath = FileSystems.getDefault().getPath(FIXTURE_PATH, req.getPathInfo()); + + // If the file exists + if (filePath.toFile().exists()) { + try { + // Set Content Type + res.setContentType(filePathToMimeType(filePath.toString())); + // Read and write to response + Files.copy(filePath, res.getOutputStream()); + + return; + } catch (NoSuchFileException nsfe) { + LOG.warning(nsfe.getClass().getName()); + } catch (IOException ioe) { + LOG.warning(ioe.getClass().getName()); + } catch (RuntimeException re) { + LOG.warning(re.getClass().getName()); + } + } + + LOG.warning("Fixture NOT FOUND: "+filePath); + res.sendError(404); //< Not Found + } + + private static String filePathToMimeType(String filePath) { + if (filePath.endsWith(".js")) { + return "application/javascript"; + } + if (filePath.endsWith(".json")) { + return "text/json"; + } + if (filePath.endsWith(".png")) { + return "image/png"; + } + if (filePath.endsWith(".jpg")) { + return "image/jpg"; + } + if (filePath.endsWith(".gif")) { + return "image/gif"; + } + + return "text/html"; + } +} diff --git a/test/ghostdriver-test/java/src/test/java/ghostdriver/server/HttpRequestCallback.java b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/HttpRequestCallback.java new file mode 100644 index 0000000..ea5c332 --- /dev/null +++ b/test/ghostdriver-test/java/src/test/java/ghostdriver/server/HttpRequestCallback.java @@ -0,0 +1,36 @@ +/* +This file is part of the GhostDriver by Ivan De Marino . + +Copyright (c) 2014, Ivan De Marino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package ghostdriver.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface HttpRequestCallback { + public void call(HttpServletRequest req, HttpServletResponse res) throws IOException; +} diff --git a/test/manual/standards/ecma-test262.js b/test/manual/standards/ecma-test262.js new file mode 100644 index 0000000..edb1994 --- /dev/null +++ b/test/manual/standards/ecma-test262.js @@ -0,0 +1,36 @@ +// Launch the official test suite for ECMA-262 + +var webpage = require('webpage'); + +page = webpage.create(); +page.onError = function() {}; + +page.open('http://test262.ecmascript.org/', function() { + page.evaluate(function() { $('a#run').click(); }); + page.evaluate(function() { $('img#btnRunAll').click(); }); + + function monitor() { + + var data = page.evaluate(function() { + return { + ran: $('#totalCounter').text(), + total: $('#testsToRun').text(), + pass: $('#Pass').text(), + fail: $('#Fail').text(), + progress: $('div#progressbar').text() + }; + }); + + console.log('Tests: ', data.ran, 'of', data.total, + ' Pass:', data.pass, ' Fail:', data.fail); + + if (data.progress.indexOf('complete') > 0) { + page.render('report.png'); + phantom.exit(); + } else { + setTimeout(monitor, 1000); + } + } + + setTimeout(monitor, 0); +}); diff --git a/test/module/cookiejar/cookiejar.js b/test/module/cookiejar/cookiejar.js new file mode 100644 index 0000000..611d554 --- /dev/null +++ b/test/module/cookiejar/cookiejar.js @@ -0,0 +1,92 @@ +var cookie0 = { + 'name': 'Valid-Cookie-Name', + 'value': 'Valid-Cookie-Value', + 'domain': 'localhost', + 'path': '/foo', + 'httponly': true, + 'secure': false +}; +var cookie1 = { + 'name': 'Valid-Cookie-Name-1', + 'value': 'Valid-Cookie-Value', + 'domain': 'localhost', + 'path': '/foo', + 'httponly': true, + 'secure': false +}; +var cookie2 = { + 'name': 'Valid-Cookie-Name-2', + 'value': 'Valid-Cookie-Value', + 'domain': 'localhost', + 'path': '/foo', + 'httponly': true, + 'secure': false +}; +var cookies = [{ + 'name': 'Valid-Cookie-Name', + 'value': 'Valid-Cookie-Value', + 'domain': 'localhost', + 'path': '/foo', + 'httponly': true, + 'secure': false +},{ + 'name': 'Valid-Cookie-Name-Sec', + 'value': 'Valid-Cookie-Value-Sec', + 'domain': 'localhost', + 'path': '/foo', + 'httponly': true, + 'secure': false, + 'expires': new Date().getTime() + 3600 //< expires in 1h +}]; + +var cookiejar, jar1, jar2; +setup(function () { + cookiejar = require('cookiejar'); + jar1 = cookiejar.create(); + jar2 = cookiejar.create(); +}); + +test(function () { + assert_type_of(jar1, 'object'); + assert_not_equals(jar1, null); + assert_type_of(jar1.cookies, 'object'); + + assert_type_of(jar1.addCookie, 'function'); + assert_type_of(jar1.deleteCookie, 'function'); + assert_type_of(jar1.clearCookies, 'function'); +}, "cookie jar properties"); + +test(function () { + assert_equals(jar1.cookies.length, 0); + + jar1.addCookie(cookie0); + assert_equals(jar1.cookies.length, 1); + + jar1.deleteCookie('Valid-Cookie-Name'); + assert_equals(jar1.cookies.length, 0); +}, "adding and removing cookies"); + +test(function () { + assert_equals(jar1.cookies.length, 0); + + jar1.cookies = cookies; + assert_equals(jar1.cookies.length, 2); + + jar1.clearCookies(); + assert_equals(jar1.cookies.length, 0); +}, "setting and clearing a cookie jar"); + +test(function () { + jar1.addCookie(cookie1); + assert_equals(jar1.cookies.length, 1); + assert_equals(jar2.cookies.length, 0); + + jar2.addCookie(cookie2); + jar1.deleteCookie('Valid-Cookie-Name-1'); + assert_equals(jar1.cookies.length, 0); + assert_equals(jar2.cookies.length, 1); + + jar1.close(); + jar2.close(); + +}, "cookie jar isolation"); diff --git a/test/module/cookiejar/to-map.js b/test/module/cookiejar/to-map.js new file mode 100644 index 0000000..7f696eb --- /dev/null +++ b/test/module/cookiejar/to-map.js @@ -0,0 +1,51 @@ +var cookies = { + 'beforeExpires': { + 'name': 'beforeExpires', + 'value': 'expireValue', + 'domain': '.abc.com', + 'path': '/', + 'httponly': false, + 'secure': false, + 'expires': 'Tue, 10 Jun 2025 12:28:29 GMT' + }, + 'noExpiresDate': { + 'name': 'noExpiresDate', + 'value': 'value', + 'domain': '.abc.com', + 'path': '/', + 'httponly': false, + 'secure': false, + 'expires': null + }, + 'afterExpires': { + 'name': 'afterExpires', + 'value': 'value', + 'domain': '.abc.com', + 'path': '/', + 'httponly': false, + 'secure': false, + 'expires': 'Mon, 10 Jun 2024 12:28:29 GMT' + } +}; + +test(function () { + var i, c, d, prop; + for (i in cookies) { + if (!cookies.hasOwnProperty(i)) continue; + phantom.addCookie(cookies[i]); + } + for (i in phantom.cookies) { + d = phantom.cookies[i]; + c = cookies[d.name]; + for (prop in c) { + if (!c.hasOwnProperty(prop)) continue; + if (c[prop] === null) { + assert_no_property(d, prop); + } else { + assert_own_property(d, prop); + assert_equals(c[prop], d[prop]); + } + } + } + +}, "optional cookie properties should not leak"); diff --git a/test/module/fs/basics.js b/test/module/fs/basics.js new file mode 100644 index 0000000..8a639a1 --- /dev/null +++ b/test/module/fs/basics.js @@ -0,0 +1,220 @@ +// Basic Files API (read, write, remove, ...) + +var FILENAME = "temp-01.test", + FILENAME_COPY = FILENAME + ".copy", + FILENAME_MOVED = FILENAME + ".moved", + FILENAME_EMPTY = FILENAME + ".empty", + FILENAME_ENC = FILENAME + ".enc", + FILENAME_BIN = FILENAME + ".bin", + ABSENT = "absent-01.test"; + +var fs; + +setup(function () { + fs = require('fs'); + var f = fs.open(FILENAME, "w"); + + f.write("hello"); + f.writeLine(""); + f.writeLine("world"); + f.close(); +}); + +test(function () { + assert_is_true(fs.exists(FILENAME)); + // we might've gotten DOS line endings + assert_greater_than_equal(fs.size(FILENAME), "hello\nworld\n".length); + +}, "create a file with contents"); + +test(function () { + assert_is_false(fs.exists(FILENAME_EMPTY)); + fs.touch(FILENAME_EMPTY); + assert_is_true(fs.exists(FILENAME_EMPTY)); + assert_equals(fs.size(FILENAME_EMPTY), 0); + +}, "create (touch) an empty file"); + +test(function () { + var content = ""; + var f = fs.open(FILENAME, "r"); + this.add_cleanup(function () { f.close(); }); + + content = f.read(); + assert_equals(content, "hello\nworld\n"); + +}, "read content from a file"); + +test(function () { + var content = ""; + var f = fs.open(FILENAME, "r"); + this.add_cleanup(function () { f.close(); }); + + f.seek(3); + content = f.read(5); + assert_equals(content, "lo\nwo"); + +}, "read specific number of bytes from a specific position in a file"); + +test(function () { + var content = ""; + var f = fs.open(FILENAME, "rw+"); + this.add_cleanup(function () { f.close(); }); + + f.writeLine("asdf"); + content = f.read(); + assert_equals(content, "hello\nworld\nasdf\n"); + +}, "append content to a file"); + +test(function () { + var f = fs.open(FILENAME, "r"); + this.add_cleanup(function () { f.close(); }); + assert_equals(f.getEncoding(), "UTF-8"); + +}, "get the file encoding (default: UTF-8)"); + +test(function () { + var f = fs.open(FILENAME, { charset: "UTF-8", mode: "r" }); + this.add_cleanup(function () { f.close(); }); + assert_equals(f.getEncoding(), "UTF-8"); + + var g = fs.open(FILENAME, { charset: "SJIS", mode: "r" }); + this.add_cleanup(function () { g.close(); }); + assert_equals(g.getEncoding(), "Shift_JIS"); + +}, "set the encoding on open"); + +test(function () { + var f = fs.open(FILENAME, { charset: "UTF-8", mode: "r" }); + this.add_cleanup(function () { f.close(); }); + assert_equals(f.getEncoding(), "UTF-8"); + f.setEncoding("utf8"); + assert_equals(f.getEncoding(), "UTF-8"); + + var g = fs.open(FILENAME, { charset: "SJIS", mode: "r" }); + this.add_cleanup(function () { g.close(); }); + assert_equals(g.getEncoding(), "Shift_JIS"); + g.setEncoding("eucjp"); + assert_equals(g.getEncoding(), "EUC-JP"); + +}, "change the encoding using setEncoding"); + +test(function () { + assert_is_false(fs.exists(FILENAME_COPY)); + fs.copy(FILENAME, FILENAME_COPY); + assert_is_true(fs.exists(FILENAME_COPY)); + assert_equals(fs.read(FILENAME), fs.read(FILENAME_COPY)); + +}, "copy a file"); + +test(function () { + assert_is_true(fs.exists(FILENAME)); + var contentBeforeMove = fs.read(FILENAME); + fs.move(FILENAME, FILENAME_MOVED); + assert_is_false(fs.exists(FILENAME)); + assert_is_true(fs.exists(FILENAME_MOVED)); + assert_equals(fs.read(FILENAME_MOVED), contentBeforeMove); + +}, "move a file"); + +test(function () { + assert_is_true(fs.exists(FILENAME_MOVED)); + assert_is_true(fs.exists(FILENAME_COPY)); + assert_is_true(fs.exists(FILENAME_EMPTY)); + + fs.remove(FILENAME_MOVED); + fs.remove(FILENAME_COPY); + fs.remove(FILENAME_EMPTY); + + assert_is_false(fs.exists(FILENAME_MOVED)); + assert_is_false(fs.exists(FILENAME_COPY)); + assert_is_false(fs.exists(FILENAME_EMPTY)); + +}, "remove a file"); + +test(function () { + assert_throws("Unable to open file '"+ ABSENT +"'", + function () { fs.open(ABSENT, "r"); }); + + assert_throws("Unable to copy file '" + ABSENT + + "' at '" + FILENAME_COPY + "'", + function () { fs.copy(ABSENT, FILENAME_COPY); }); + +}, "operations on nonexistent files throw an exception"); + +test(function () { + var data = "ÄABCÖ"; + var data_b = String.fromCharCode( + 0xC3, 0x84, 0x41, 0x42, 0x43, 0xC3, 0x96); + + var f = fs.open(FILENAME_ENC, "w"); + this.add_cleanup(function () { + f.close(); + fs.remove(FILENAME_ENC); + }); + + f.write(data); + f.close(); + + f = fs.open(FILENAME_ENC, "r"); + assert_equals(f.read(), data); + + var g = fs.open(FILENAME_ENC, "rb"); + this.add_cleanup(function () { g.close(); }); + assert_equals(g.read(), data_b); + +}, "read/write UTF-8 text by default"); + +test(function () { + var data = "ピタゴラスイッチ"; + var data_b = String.fromCharCode( + 0x83, 0x73, 0x83, 0x5e, 0x83, 0x53, 0x83, 0x89, + 0x83, 0x58, 0x83, 0x43, 0x83, 0x62, 0x83, 0x60); + + var f = fs.open(FILENAME_ENC, { mode: "w", charset: "Shift_JIS" }); + this.add_cleanup(function () { + f.close(); + fs.remove(FILENAME_ENC); + }); + + f.write(data); + f.close(); + + f = fs.open(FILENAME_ENC, { mode: "r", charset: "Shift_JIS" }); + assert_equals(f.read(), data); + + var g = fs.open(FILENAME_ENC, "rb"); + this.add_cleanup(function () { g.close(); }); + assert_equals(g.read(), data_b); + +}, "read/write Shift-JIS text with options"); + +test(function () { + var data = String.fromCharCode(0, 1, 2, 3, 4, 5); + + var f = fs.open(FILENAME_BIN, "wb"); + this.add_cleanup(function () { + f.close(); + fs.remove(FILENAME_BIN); + }); + + f.write(data); + f.close(); + + f = fs.open(FILENAME_BIN, "rb"); + assert_equals(f.read(), data); + +}, "read/write binary data"); + +test(function () { + var data = String.fromCharCode(0, 1, 2, 3, 4, 5); + + fs.write(FILENAME_BIN, data, "b"); + this.add_cleanup(function () { + fs.remove(FILENAME_BIN); + }); + + assert_equals(fs.read(FILENAME_BIN, "b"), data); + +}, "read/write binary data (shortcuts)"); diff --git a/test/module/fs/fileattrs.js b/test/module/fs/fileattrs.js new file mode 100644 index 0000000..783b8fa --- /dev/null +++ b/test/module/fs/fileattrs.js @@ -0,0 +1,91 @@ + +var fs = require('fs'); + +var ABSENT_DIR = "absentdir02", + ABSENT_FILE = "absentfile02", + TEST_DIR = "testdir02", + TEST_FILE = "temp-02.test", + TEST_FILE_PATH = fs.join(TEST_DIR, TEST_FILE), + TEST_CONTENT = "test content", + CONTENT_MULTIPLIER = 1024; + +test(function () { + assert_throws("Unable to read file '"+ ABSENT_FILE +"' size", + function () { fs.size(ABSENT_FILE); }); + + assert_equals(fs.lastModified(ABSENT_FILE), null); + +}, "size/date queries on nonexistent files"); + +test(function () { + // Round down to the nearest multiple of two seconds, because + // file timestamps might only have that much precision. + var before_creation = Math.floor(Date.now() / 2000) * 2000; + + var f = fs.open(TEST_FILE, "w"); + this.add_cleanup(function () { + if (f !== null) f.close(); + fs.remove(TEST_FILE); + }); + + for (var i = 0; i < CONTENT_MULTIPLIER; i++) { + f.write(TEST_CONTENT); + } + f.close(); f = null; + + // Similarly, but round _up_. + var after_creation = Math.ceil(Date.now() / 2000) * 2000; + + assert_equals(fs.size(TEST_FILE), + TEST_CONTENT.length * CONTENT_MULTIPLIER); + + var flm = fs.lastModified(TEST_FILE).getTime(); + + assert_greater_than_equal(flm, before_creation); + assert_less_than_equal(flm, after_creation); + +}, "size/date queries on existing files"); + +test(function () { + fs.makeDirectory(TEST_DIR); + this.add_cleanup(function () { fs.removeTree(TEST_DIR); }); + fs.write(TEST_FILE_PATH, TEST_CONTENT, "w"); + + assert_is_true(fs.exists(TEST_FILE_PATH)); + assert_is_true(fs.exists(TEST_DIR)); + assert_is_false(fs.exists(ABSENT_FILE)); + assert_is_false(fs.exists(ABSENT_DIR)); + + assert_is_true(fs.isDirectory(TEST_DIR)); + assert_is_false(fs.isDirectory(ABSENT_DIR)); + + + assert_is_true(fs.isFile(TEST_FILE_PATH)); + assert_is_false(fs.isFile(ABSENT_FILE)); + + var absPath = fs.absolute(TEST_FILE_PATH); + assert_is_false(fs.isAbsolute(TEST_FILE_PATH)); + assert_is_true(fs.isAbsolute(absPath)); + + assert_is_true(fs.isReadable(TEST_FILE_PATH)); + assert_is_true(fs.isWritable(TEST_FILE_PATH)); + assert_is_false(fs.isExecutable(TEST_FILE_PATH)); + + assert_is_false(fs.isReadable(ABSENT_FILE)); + assert_is_false(fs.isWritable(ABSENT_FILE)); + assert_is_false(fs.isExecutable(ABSENT_FILE)); + + assert_is_true(fs.isReadable(TEST_DIR)); + assert_is_true(fs.isWritable(TEST_DIR)); + assert_is_true(fs.isExecutable(TEST_DIR)); + + assert_is_false(fs.isReadable(ABSENT_DIR)); + assert_is_false(fs.isWritable(ABSENT_DIR)); + assert_is_false(fs.isExecutable(ABSENT_DIR)); + + assert_is_false(fs.isLink(TEST_DIR)); + assert_is_false(fs.isLink(TEST_FILE_PATH)); + assert_is_false(fs.isLink(ABSENT_DIR)); + assert_is_false(fs.isLink(ABSENT_FILE)); + +}, "file types and access modes"); diff --git a/test/module/fs/paths.js b/test/module/fs/paths.js new file mode 100644 index 0000000..0910f20 --- /dev/null +++ b/test/module/fs/paths.js @@ -0,0 +1,72 @@ + +var fs = require('fs'); +var system = require('system'); + +var TEST_DIR = "testdir", + TEST_FILE = "testfile", + START_CWD = fs.workingDirectory; + +test(function () { + assert_is_true(fs.makeDirectory(TEST_DIR)); + this.add_cleanup(function () { fs.removeTree(TEST_DIR); }); + + assert_is_true(fs.changeWorkingDirectory(TEST_DIR)); + this.add_cleanup(function () { fs.changeWorkingDirectory(START_CWD); }); + + fs.write(TEST_FILE, TEST_FILE, "w"); + var suffix = fs.join("", TEST_DIR, TEST_FILE), + abs = fs.absolute(".." + suffix), + lastIndex = abs.lastIndexOf(suffix); + + assert_not_equals(lastIndex, -1); + assert_equals(lastIndex + suffix.length, abs.length); + +}, "manipulation of current working directory"); + +test(function () { + + fs.copyTree(phantom.libraryPath, TEST_DIR); + this.add_cleanup(function () { fs.removeTree(TEST_DIR); }); + + assert_deep_equals(fs.list(phantom.libraryPath), fs.list(TEST_DIR)); + +}, "copying a directory tree"); + +test(function () { + assert_type_of(fs.readLink, 'function'); + // TODO: test the actual functionality once we can create symlinks. +}, "fs.readLink exists"); + +generate_tests(function fs_join_test (parts, expected) { + var actual = fs.join.apply(null, parts); + assert_equals(actual, expected); +}, [ + [ "fs.join: []", [], "." ], + [ "fs.join: nonsense", [[], null], "." ], + [ "fs.join: 1 element", [""], "." ], + [ "fs.join: 2 elements", ["", "a"], "/a" ], + [ "fs.join: 3 elements", ["a", "b", "c"], "a/b/c" ], + [ "fs.join: 4 elements", ["", "a", "b", "c"], "/a/b/c" ], + [ "fs.join: empty elements", ["", "a", "", "b", "", "c"], "/a/b/c" ], + [ "fs.join: empty elements 2", ["a", "", "b", "", "c"], "a/b/c" ] +]); + +generate_tests(function fs_split_test (input, expected) { + var path = input.join(fs.separator); + var actual = fs.split(path); + assert_deep_equals(actual, expected); +}, [ + [ "fs.split: absolute", + ["", "a", "b", "c", "d"], ["", "a", "b", "c", "d"] ], + [ "fs.split: absolute, trailing", + ["", "a", "b", "c", "d", ""], ["", "a", "b", "c", "d"] ], + [ "fs.split: non-absolute", + ["a", "b", "c", "d"], ["a", "b", "c", "d"] ], + [ "fs.split: non-absolute, trailing", + ["a", "b", "c", "d", ""], ["a", "b", "c", "d"] ], + [ "fs.split: repeated separators", + ["a", "", "", "", + "b", "", + "c", "", "", + "d", "", "", ""], ["a", "b", "c", "d"] ] +]); diff --git a/test/module/system/stdin.js b/test/module/system/stdin.js new file mode 100644 index 0000000..8a08f26 --- /dev/null +++ b/test/module/system/stdin.js @@ -0,0 +1,22 @@ +//! stdin: Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich +//! stdin: いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす + +//^ first line: pangram in German +//^ second line: pan+isogram in hiragana (the Iroha) + +var stdin; +setup(function () { stdin = require("system").stdin; }); + +test(function () { + assert_equals(stdin.readLine(), + "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"); +}, "input line one (German)"); + +test(function () { + assert_equals(stdin.readLine(), + "いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす"); +}, "input line two (Japanese)"); + +test(function () { + assert_equals(stdin.readLine(), ""); +}, "input line three (EOF)"); diff --git a/test/module/system/stdout-err.js b/test/module/system/stdout-err.js new file mode 100644 index 0000000..2281b42 --- /dev/null +++ b/test/module/system/stdout-err.js @@ -0,0 +1,14 @@ +//! no-harness +//! expect-stdout: Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich +//! expect-stderr: いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす + +//^ stdout: pangram in German +//^ stderr: pan+isogram in hiragana (the Iroha) + +phantom.onError = function () { phantom.exit(1); }; + +var sys = require("system"); + +sys.stdout.write("Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich\n"); +sys.stderr.write("いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす"); +phantom.exit(0); diff --git a/test/module/system/system.js b/test/module/system/system.js new file mode 100644 index 0000000..0d4d4e5 --- /dev/null +++ b/test/module/system/system.js @@ -0,0 +1,77 @@ +var system = require('system'); + +test(function () { + assert_type_of(system, 'object'); + assert_not_equals(system, null); +}, "system object"); + +test(function () { + assert_own_property(system, 'pid'); + assert_type_of(system.pid, 'number'); + assert_greater_than(system.pid, 0); +}, "system.pid"); + +test(function () { + assert_own_property(system, 'isSSLSupported'); + assert_type_of(system.isSSLSupported, 'boolean'); + assert_equals(system.isSSLSupported, true); +}, "system.isSSLSupported"); + +test(function () { + assert_own_property(system, 'args'); + assert_type_of(system.args, 'object'); + assert_instance_of(system.args, Array); + assert_greater_than_equal(system.args.length, 1); + + // args[0] will be the test harness. + assert_regexp_match(system.args[0], /\btestharness\.js$/); +}, "system.args"); + +test(function () { + assert_own_property(system, 'env'); + assert_type_of(system.env, 'object'); +}, "system.env"); + +test(function () { + assert_own_property(system, 'platform'); + assert_type_of(system.platform, 'string'); + assert_equals(system.platform, 'phantomjs'); +}, "system.platform"); + +test(function () { + assert_own_property(system, 'os'); + assert_type_of(system.os, 'object'); + + assert_type_of(system.os.architecture, 'string'); + assert_type_of(system.os.name, 'string'); + assert_type_of(system.os.version, 'string'); + + if (system.os.name === 'mac') { + // release is x.y.z with x = 10 for Snow Leopard and 14 for Yosemite + assert_type_of(system.os.release, 'string'); + assert_greater_than_equal(parseInt(system.os.release, 10), 10); + } +}, "system.os"); + +test(function () { + assert_type_of(system.stdin, 'object'); + assert_type_of(system.stdin.read, 'function'); + assert_type_of(system.stdin.readLine, 'function'); + assert_type_of(system.stdin.close, 'function'); +}, "system.stdin"); + +test(function () { + assert_type_of(system.stdout, 'object'); + assert_type_of(system.stdout.write, 'function'); + assert_type_of(system.stdout.writeLine, 'function'); + assert_type_of(system.stdout.flush, 'function'); + assert_type_of(system.stdout.close, 'function'); +}, "system.stdout"); + +test(function () { + assert_type_of(system.stderr, 'object'); + assert_type_of(system.stderr.write, 'function'); + assert_type_of(system.stderr.writeLine, 'function'); + assert_type_of(system.stderr.flush, 'function'); + assert_type_of(system.stderr.close, 'function'); +}, "system.stderr"); diff --git a/test/module/webpage/abort-network-request.js b/test/module/webpage/abort-network-request.js new file mode 100644 index 0000000..9ec906d --- /dev/null +++ b/test/module/webpage/abort-network-request.js @@ -0,0 +1,36 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + var abortCount = 0; + var errorCount = 0; + var abortedIds = {}; + var urlToBlockRegExp = /logo\.png$/i; + + page.onResourceRequested = this.step_func(function(requestData, request) { + assert_type_of(request, 'object'); + assert_type_of(request.abort, 'function'); + if (urlToBlockRegExp.test(requestData.url)) { + request.abort(); + ++abortCount; + abortedIds[requestData.id] = 1; + } + }); + page.onResourceError = this.step_func(function(error) { + // We can't match up errors to requests by URL because error.url will + // be the empty string in this case. FIXME. + assert_own_property(abortedIds, error.id); + ++errorCount; + }); + page.onResourceReceived = this.step_func(function(response) { + assert_regexp_not_match(response.url, urlToBlockRegExp); + }); + + page.open(TEST_HTTP_BASE + 'logo.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(abortCount, 1); + assert_equals(errorCount, 1); + })); + +}, "can abort network requests"); diff --git a/test/module/webpage/add-header.js b/test/module/webpage/add-header.js new file mode 100644 index 0000000..46b70bb --- /dev/null +++ b/test/module/webpage/add-header.js @@ -0,0 +1,24 @@ +async_test(function () { + var webpage = require('webpage'); + + // NOTE: HTTP header names are case-insensitive. Our test server + // returns the name in lowercase. + + var page = webpage.create(); + assert_type_of(page.customHeaders, 'object'); + assert_deep_equals(page.customHeaders, {}); + + page.onResourceRequested = this.step_func(function(requestData, request) { + assert_type_of(request.setHeader, 'function'); + request.setHeader('CustomHeader', 'CustomValue'); + }); + page.open(TEST_HTTP_BASE + 'echo', this.step_func_done(function (status) { + var json, headers; + assert_equals(status, 'success'); + json = JSON.parse(page.plainText); + headers = json.headers; + assert_own_property(headers, 'customheader'); + assert_equals(headers.customheader, 'CustomValue'); + })); + +}, "add custom headers in onResourceRequested"); diff --git a/test/module/webpage/callback.js b/test/module/webpage/callback.js new file mode 100644 index 0000000..0c14c44 --- /dev/null +++ b/test/module/webpage/callback.js @@ -0,0 +1,16 @@ +test(function () { + var page = require('webpage').create(); + + var msgA = "a", + msgB = "b", + result, + expected = msgA + msgB; + page.onCallback = function(a, b) { + return a + b; + }; + result = page.evaluate(function(a, b) { + return window.callPhantom(a, b); + }, msgA, msgB); + + assert_equals(result, expected); +}, "page.onCallback"); diff --git a/test/module/webpage/capture-content.js b/test/module/webpage/capture-content.js new file mode 100644 index 0000000..fa2e44e --- /dev/null +++ b/test/module/webpage/capture-content.js @@ -0,0 +1,49 @@ +var content; +setup(function () { + var fs = require('fs'); + // libraryPath is test/module/webpage + content = fs.read(fs.join(phantom.libraryPath, + "../../www/hello.html")); +}); + +// XFAIL: This feature had to be backed out for breaking WebSockets. +async_test(function () { + var page = require('webpage').create(); + var lastChunk = ""; + var bodySize = 0; + page.captureContent = ['.*']; + // Not a step function because it may be called several times + // and doesn't need to make assertions. + page.onResourceReceived = function (resource) { + lastChunk = resource.body; + bodySize = resource.bodySize; + }; + page.open(TEST_HTTP_BASE + "hello.html", + this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(bodySize, content.length); + assert_equals(lastChunk, content); + })); + +}, "onResourceReceived sees the body if captureContent is activated", + { expected_fail: true } +); + +async_test(function () { + var page = require('webpage').create(); + var lastChunk = ""; + var bodySize = 0; + page.captureContent = ['/some/other/url']; + // Not a step function because it may be called several times + // and doesn't need to make assertions. + page.onResourceReceived = function (resource) { + lastChunk = resource.body; + bodySize = resource.bodySize; + }; + page.open(TEST_HTTP_BASE + "hello.html", + this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(bodySize, 0); + assert_equals(lastChunk, ""); + })); +}, "onResourceReceived doesn't see the body if captureContent doesn't match"); diff --git a/test/module/webpage/change-request-encoded-url.js b/test/module/webpage/change-request-encoded-url.js new file mode 100644 index 0000000..37778a7 --- /dev/null +++ b/test/module/webpage/change-request-encoded-url.js @@ -0,0 +1,26 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + + var url = TEST_HTTP_BASE + "cdn-cgi/pe/bag?r%5B%5D="+ + "http%3A%2F%2Fwww.example.org%2Fcdn-cgi%2Fnexp%2F"+ + "abv%3D927102467%2Fapps%2Fabetterbrowser.js"; + var receivedUrl; + + page.onResourceRequested = this.step_func(function(requestData, request) { + request.changeUrl(requestData.url); + }); + + page.onResourceReceived = this.step_func(function(data) { + if (data.stage === 'end') { + receivedUrl = data.url; + } + }); + + page.open(url, this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(receivedUrl, url); + })); + +}, "encoded URLs properly round-trip through request.changeUrl"); diff --git a/test/module/webpage/change-request-url.js b/test/module/webpage/change-request-url.js new file mode 100644 index 0000000..3e2bddb --- /dev/null +++ b/test/module/webpage/change-request-url.js @@ -0,0 +1,39 @@ +var webpage = require('webpage'); + +async_test(function () { + + var page = webpage.create(); + var urlToChange = TEST_HTTP_BASE + 'logo.png'; + var alternativeUrl = TEST_HTTP_BASE + 'phantomjs-logo.gif'; + var startStage = 0; + var endStage = 0; + + page.onResourceRequested = this.step_func(function(requestData, request) { + if (requestData.url === urlToChange) { + assert_type_of(request, 'object'); + assert_type_of(request.changeUrl, 'function'); + request.changeUrl(alternativeUrl); + } + }); + + page.onResourceReceived = this.step_func(function(data) { + if (data.url === alternativeUrl && data.stage === 'start') { + ++startStage; + } + if (data.url === alternativeUrl && data.stage === 'end') { + ++endStage; + } + }); + + page.open(TEST_HTTP_BASE + 'logo.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(startStage, 1); + assert_equals(endStage, 1); + + // The page HTML should still refer to the original image. + assert_regexp_match(page.content, /logo\.png/); + assert_regexp_not_match(page.content, /logo\.gif/); + })); + +}, "request.changeUrl"); diff --git a/test/module/webpage/cjk-text-codecs.js b/test/module/webpage/cjk-text-codecs.js new file mode 100644 index 0000000..afd653c --- /dev/null +++ b/test/module/webpage/cjk-text-codecs.js @@ -0,0 +1,33 @@ +var webpage = require('webpage'); + +function test_one(text) { + var t = async_test(text.codec); + t.step(function () { + var page = webpage.create(); + page.open(text.url, t.step_func_done(function () { + var decodedText = page.evaluate(function() { + return document.querySelector('pre').innerText; + }); + var regex = '^' + text.reference + '$'; + assert_regexp_match(text.reference, new RegExp(regex)); + })); + }); +} + +function Text(codec, base64, reference) { + this.codec = codec; + this.base64 = base64; + this.reference = reference; + this.url = 'data:text/plain;charset=' + this.codec + + ';base64,' + this.base64; +} + +[ + new Text('Shift_JIS', 'g3SDQIOTg2eDgA==', 'ファントム'), + new Text('EUC-JP', 'pdWloaXzpcil4A0K', 'ファントム'), + new Text('ISO-2022-JP', 'GyRCJVUlISVzJUglYBsoQg0K', 'ファントム'), + new Text('Big5', 'pNu2SA0K', '幻象'), + new Text('GBK', 'u8PP8w0K', '幻象'), + new Text('EUC-KR', 'yK+/tQ==', '환영'), +] + .forEach(test_one); diff --git a/test/module/webpage/clip-rect.js b/test/module/webpage/clip-rect.js new file mode 100644 index 0000000..46c9e40 --- /dev/null +++ b/test/module/webpage/clip-rect.js @@ -0,0 +1,19 @@ +var webpage = require('webpage'); + +test(function () { + var defaultPage = webpage.create(); + assert_deep_equals(defaultPage.clipRect, {height:0,left:0,top:0,width:0}); +}, "default page.clipRect"); + +test(function () { + var options = { + clipRect: { + height: 100, + left: 10, + top: 20, + width: 200 + } + }; + var customPage = webpage.create(options); + assert_deep_equals(customPage.clipRect, options.clipRect); +}, "custom page.clipRect"); diff --git a/test/module/webpage/construction-with-options.js b/test/module/webpage/construction-with-options.js new file mode 100644 index 0000000..22e6913 --- /dev/null +++ b/test/module/webpage/construction-with-options.js @@ -0,0 +1,58 @@ +test(function () { + var opts = {}, + page = new WebPage(opts); + assert_type_of(page, 'object'); + assert_not_equals(page, null); +}, "webpage constructor accepts an opts object"); + +async_test(function () { + var opts = { + onConsoleMessage: this.step_func_done(function (msg) { + assert_equals(msg, "test log"); + }) + }; + var page = new WebPage(opts); + assert_equals(page.onConsoleMessage, opts.onConsoleMessage); + page.evaluate(function () {console.log('test log');}); + +}, "specifying onConsoleMessage with opts"); + +async_test(function () { + var page_opened = false; + var opts = { + onLoadStarted: this.step_func_done(function (msg) { + assert_is_true(page_opened); + }) + }; + var page = new WebPage(opts); + assert_equals(page.onLoadStarted, opts.onLoadStarted); + page_opened = true; + page.open("about:blank"); + +}, "specifying onLoadStarted with opts"); + +async_test(function () { + var page_opened = false; + var opts = { + onLoadFinished: this.step_func_done(function (msg) { + assert_is_true(page_opened); + }) + }; + var page = new WebPage(opts); + assert_equals(page.onLoadFinished, opts.onLoadFinished); + page_opened = true; + page.open("about:blank"); + +}, "specifying onLoadFinished with opts"); + +// FIXME: Actually test that the timeout is effective. +test(function () { + var opts = { + settings: { + timeout: 100 // time in ms + } + }; + var page = new WebPage(opts); + assert_equals(page.settings.timeout, opts.settings.timeout); + +}, "specifying timeout with opts"); diff --git a/test/module/webpage/contextclick-event.js b/test/module/webpage/contextclick-event.js new file mode 100644 index 0000000..d031746 --- /dev/null +++ b/test/module/webpage/contextclick-event.js @@ -0,0 +1,32 @@ +test(function () { + var page = require('webpage').create(); + + page.evaluate(function() { + window.addEventListener('contextmenu', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.contextmenu = event; + }, false); + }); + page.sendEvent('contextmenu', 42, 217); + + var event = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(event.contextmenu.clientX, 42); + assert_equals(event.contextmenu.clientY, 217); + + // click with modifier key + page.evaluate(function() { + window.addEventListener('contextmenu', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.contextmenu = event; + }, false); + }); + page.sendEvent('contextmenu', 100, 100, 'left', page.event.modifier.shift); + + var event = page.evaluate(function() { + return window.loggedEvent.contextmenu; + }); + assert_is_true(event.shiftKey); + +}, "context click events"); diff --git a/test/module/webpage/cookies.js b/test/module/webpage/cookies.js new file mode 100644 index 0000000..c85cfb6 --- /dev/null +++ b/test/module/webpage/cookies.js @@ -0,0 +1,111 @@ +async_test(function () { + var url = TEST_HTTP_BASE + "echo"; + var page = new WebPage(); + + page.cookies = [{ + 'name' : 'Valid-Cookie-Name', + 'value' : 'Valid-Cookie-Value', + 'domain' : 'localhost', + 'path' : '/', + 'httponly' : true, + 'secure' : false + },{ + 'name' : 'Valid-Cookie-Name-Sec', + 'value' : 'Valid-Cookie-Value-Sec', + 'domain' : 'localhost', + 'path' : '/', + 'httponly' : true, + 'secure' : false, + 'expires' : Date.now() + 3600 //< expires in 1h + }]; + + page.open(url, this.step_func(function (status) { + assert_equals(status, "success"); + var headers = JSON.parse(page.plainText).headers; + assert_own_property(headers, 'cookie'); + assert_regexp_match(headers.cookie, /\bValid-Cookie-Name\b/); + assert_regexp_match(headers.cookie, /\bValid-Cookie-Value\b/); + assert_regexp_match(headers.cookie, /\bValid-Cookie-Name-Sec\b/); + assert_regexp_match(headers.cookie, /\bValid-Cookie-Value-Sec\b/); + assert_not_equals(page.cookies.length, 0); + + page.cookies = []; + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + var headers = JSON.parse(page.plainText).headers; + assert_no_property(headers, 'cookie'); + })); + })); +}, "adding and deleting cookies with page.cookies"); + +async_test(function () { + var url = TEST_HTTP_BASE + "echo"; + var page = new WebPage(); + + page.addCookie({ + 'name' : 'Added-Cookie-Name', + 'value' : 'Added-Cookie-Value', + 'domain' : 'localhost' + }); + + page.open(url, this.step_func(function (status) { + assert_equals(status, "success"); + var headers = JSON.parse(page.plainText).headers; + assert_own_property(headers, 'cookie'); + assert_regexp_match(headers.cookie, /\bAdded-Cookie-Name\b/); + assert_regexp_match(headers.cookie, /\bAdded-Cookie-Value\b/); + + page.deleteCookie("Added-Cookie-Name"); + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + var headers = JSON.parse(page.plainText).headers; + assert_no_property(headers, 'cookie'); + })); + })); + +}, "adding and deleting cookies with page.addCookie and page.deleteCookie"); + +async_test(function () { + var url = TEST_HTTP_BASE + "echo"; + var page = new WebPage(); + + page.cookies = [ + { // domain mismatch. + 'name' : 'Invalid-Cookie-Name-1', + 'value' : 'Invalid-Cookie-Value-1', + 'domain' : 'foo.example' + },{ // path mismatch: the cookie will be set, + // but won't be visible from the given URL (not same path). + 'name' : 'Invalid-Cookie-Name-2', + 'value' : 'Invalid-Cookie-Value-2', + 'domain' : 'localhost', + 'path' : '/bar' + },{ // cookie expired. + 'name' : 'Invalid-Cookie-Name-3', + 'value' : 'Invalid-Cookie-Value-3', + 'domain' : 'localhost', + 'expires' : 'Sat, 01 Jan 2000 00:00:00 GMT' + },{ // https only: the cookie will be set, + // but won't be visible from the given URL (not https). + 'name' : 'Invalid-Cookie-Name-4', + 'value' : 'Invalid-Cookie-Value-4', + 'domain' : 'localhost', + 'secure' : true + },{ // cookie expired (date in "sec since epoch"). + 'name' : 'Invalid-Cookie-Name-5', + 'value' : 'Invalid-Cookie-Value-5', + 'domain' : 'localhost', + 'expires' : new Date().getTime() - 1 //< date in the past + },{ // cookie expired (date in "sec since epoch" - using "expiry"). + 'name' : 'Invalid-Cookie-Name-6', + 'value' : 'Invalid-Cookie-Value-6', + 'domain' : 'localhost', + 'expiry' : new Date().getTime() - 1 //< date in the past + }]; + + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + var headers = JSON.parse(page.plainText).headers; + assert_no_property(headers, 'cookie'); + })); +}, "page.cookies provides cookies only to appropriate requests"); diff --git a/test/module/webpage/custom-headers.js b/test/module/webpage/custom-headers.js new file mode 100644 index 0000000..058748c --- /dev/null +++ b/test/module/webpage/custom-headers.js @@ -0,0 +1,30 @@ +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + assert_type_of(page.customHeaders, 'object'); + assert_deep_equals(page.customHeaders, {}); + + // NOTE: HTTP header names are case-insensitive. Our test server + // returns the name in lowercase. + page.customHeaders = { + 'Custom-Key': 'Custom-Value', + 'User-Agent': 'Overriden-UA', + 'Referer': 'Overriden-Referer' + }; + page.open(TEST_HTTP_BASE + 'echo', this.step_func_done(function (status) { + var json, headers; + assert_equals(status, 'success'); + json = JSON.parse(page.plainText); + assert_type_of(json, 'object'); + headers = json.headers; + assert_type_of(headers, 'object'); + + assert_own_property(headers, 'custom-key'); + assert_own_property(headers, 'user-agent'); + assert_own_property(headers, 'referer'); + assert_equals(headers['custom-key'], 'Custom-Value'); + assert_equals(headers['user-agent'], 'Overriden-UA'); + assert_equals(headers['referer'], 'Overriden-Referer'); + })); + +}, "adding custom headers with page.customHeaders"); diff --git a/test/module/webpage/evaluate-broken-json.js b/test/module/webpage/evaluate-broken-json.js new file mode 100644 index 0000000..1f1d263 --- /dev/null +++ b/test/module/webpage/evaluate-broken-json.js @@ -0,0 +1,14 @@ +test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + // Hijack JSON.parse to something completely useless. + page.content = ''; + + var result = page.evaluate(function(obj) { + return obj.value * obj.value; + }, { value: 4 }); + + assert_equals(result, 16); + +}, "page script should not interfere with page.evaluate"); diff --git a/test/module/webpage/file-upload.js b/test/module/webpage/file-upload.js new file mode 100644 index 0000000..448e7ad --- /dev/null +++ b/test/module/webpage/file-upload.js @@ -0,0 +1,55 @@ + +// Note: uses various files in module/webpage as things to be uploaded. +// Which files they are doesn't matter. + +var page; +setup(function () { + page = new WebPage(); + page.content = + '\n' + + '\n' + + '' + + ''; + page.uploadFile("#file", "file-upload.js"); + page.uploadFile("#file2", "file-upload.js"); + page.uploadFile("#file3", ["file-upload.js", "object.js"]); +}); + +function test_one_elt(id, names) { + var files = page.evaluate(function (id) { + var elt = document.getElementById(id); + var rv = []; + for (var i = 0; i < elt.files.length; i++) { + rv.push(elt.files[i].fileName); + } + return rv; + }, id); + assert_deep_equals(files, names); +} + +generate_tests(test_one_elt, [ + ["single upload single file", "file", ["file-upload.js"]], + ["multiple upload single file", "file2", ["file-upload.js"]], + ["multiple upload multiple file", "file3", ["file-upload.js", "object.js"]], +], { expected_fail: true }); + +async_test(function () { + page.onFilePicker = this.step_func(function (oldFile) { + assert_equals(oldFile, ""); + return "no-plugin.js"; + }); + + test_one_elt("file4", []); + + page.evaluate(function () { + var fileUp = document.querySelector("#file4"); + var ev = document.createEvent("MouseEvents"); + ev.initEvent("click", true, true); + fileUp.dispatchEvent(ev); + }); + + setTimeout(this.step_func_done(function () { + test_one_elt("file4", ["no-plugin.js"]); + }, 0)); + +}, "page.onFilePicker", { expected_fail: true }); diff --git a/test/module/webpage/frame-switching-deprecated.js b/test/module/webpage/frame-switching-deprecated.js new file mode 100644 index 0000000..54e5b74 --- /dev/null +++ b/test/module/webpage/frame-switching-deprecated.js @@ -0,0 +1,68 @@ +async_test(function () { + var p = require("webpage").create(); + + function pageTitle(page) { + return page.evaluate(function(){ + return window.document.title; + }); + } + + function setPageTitle(page, newTitle) { + page.evaluate(function(newTitle){ + window.document.title = newTitle; + }, newTitle); + } + + function testFrameSwitchingDeprecated() { + assert_equals(pageTitle(p), "index"); + assert_equals(p.currentFrameName(), ""); + assert_equals(p.childFramesCount(), 2); + assert_deep_equals(p.childFramesName(), ["frame1", "frame2"]); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToChildFrame("frame1")); + assert_equals(pageTitle(p), "frame1"); + assert_equals(p.currentFrameName(), "frame1"); + assert_equals(p.childFramesCount(), 2); + assert_deep_equals(p.childFramesName(), ["frame1-1", "frame1-2"]); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToChildFrame("frame1-2")); + assert_equals(pageTitle(p), "frame1-2"); + assert_equals(p.currentFrameName(), "frame1-2"); + assert_equals(p.childFramesCount(), 0); + assert_deep_equals(p.childFramesName(), []); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToParentFrame()); + assert_equals(pageTitle(p), "frame1-visited"); + assert_equals(p.currentFrameName(), "frame1"); + assert_equals(p.childFramesCount(), 2); + assert_deep_equals(p.childFramesName(), ["frame1-1", "frame1-2"]); + + assert_is_true(p.switchToChildFrame(0)); + assert_equals(pageTitle(p), "frame1-1"); + assert_equals(p.currentFrameName(), "frame1-1"); + assert_equals(p.childFramesCount(), 0); + assert_deep_equals(p.childFramesName(), []); + + assert_equals(p.switchToMainFrame(), undefined); + assert_equals(pageTitle(p), "index-visited"); + assert_equals(p.currentFrameName(), ""); + assert_equals(p.childFramesCount(), 2); + assert_deep_equals(p.childFramesName(), ["frame1", "frame2"]); + + assert_is_true(p.switchToChildFrame("frame2")); + assert_equals(pageTitle(p), "frame2"); + assert_equals(p.currentFrameName(), "frame2"); + assert_equals(p.childFramesCount(), 3); + assert_deep_equals(p.childFramesName(), + ["frame2-1", "frame2-2", "frame2-3"]); + } + + p.open(TEST_HTTP_BASE + "frameset", this.step_func_done(function (s) { + assert_equals(s, "success"); + testFrameSwitchingDeprecated(); + })); + +}, "frame switching deprecated API"); diff --git a/test/module/webpage/frame-switching.js b/test/module/webpage/frame-switching.js new file mode 100644 index 0000000..a821320 --- /dev/null +++ b/test/module/webpage/frame-switching.js @@ -0,0 +1,97 @@ +async_test(function () { + var p = require("webpage").create(); + + function pageTitle(page) { + return page.evaluate(function(){ + return window.document.title; + }); + } + + function setPageTitle(page, newTitle) { + page.evaluate(function(newTitle){ + window.document.title = newTitle; + }, newTitle); + } + + function testFrameSwitching() { + assert_equals(pageTitle(p), "index"); + assert_equals(p.frameName, ""); + assert_equals(p.framesCount, 2); + assert_deep_equals(p.framesName, ["frame1", "frame2"]); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToFrame("frame1")); + assert_equals(pageTitle(p), "frame1"); + assert_equals(p.frameName, "frame1"); + assert_equals(p.framesCount, 2); + assert_deep_equals(p.framesName, ["frame1-1", "frame1-2"]); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToFrame("frame1-2")); + assert_equals(pageTitle(p), "frame1-2"); + assert_equals(p.frameName, "frame1-2"); + assert_equals(p.framesCount, 0); + assert_deep_equals(p.framesName, []); + setPageTitle(p, pageTitle(p) + "-visited"); + + assert_is_true(p.switchToParentFrame()); + assert_equals(pageTitle(p), "frame1-visited"); + assert_equals(p.frameName, "frame1"); + assert_equals(p.framesCount, 2); + assert_deep_equals(p.framesName, ["frame1-1", "frame1-2"]); + + assert_is_true(p.switchToFrame(0)); + assert_equals(pageTitle(p), "frame1-1"); + assert_equals(p.frameName, "frame1-1"); + assert_equals(p.framesCount, 0); + assert_deep_equals(p.framesName, []); + + assert_equals(p.switchToMainFrame(), undefined); + assert_equals(pageTitle(p), "index-visited"); + assert_equals(p.frameName, ""); + assert_equals(p.framesCount, 2); + assert_deep_equals(p.framesName, ["frame1", "frame2"]); + + assert_is_true(p.switchToFrame("frame2")); + assert_equals(pageTitle(p), "frame2"); + assert_equals(p.frameName, "frame2"); + assert_equals(p.framesCount, 3); + assert_deep_equals(p.framesName, + ["frame2-1", "frame2-2", "frame2-3"]); + + assert_equals(p.focusedFrameName, ""); + + p.evaluate(function(){ + window.focus(); + }); + assert_equals(p.focusedFrameName, "frame2"); + + assert_is_true(p.switchToFrame("frame2-1")); + p.evaluate(function(){ + window.focus(); + }); + assert_equals(p.focusedFrameName, "frame2-1"); + + assert_equals(p.switchToMainFrame(), undefined); + p.evaluate(function(){ + window.focus(); + }); + assert_equals(p.focusedFrameName, ""); + + p.evaluate(function(){ + window.frames[0].focus(); + }); + assert_equals(p.focusedFrameName, "frame1"); + assert_equals(p.frameName, ""); + + assert_equals(p.switchToFocusedFrame(), undefined); + assert_equals(p.frameName, "frame1"); + } + + p.open(TEST_HTTP_BASE + "frameset", + this.step_func_done(function (s) { + assert_equals(s, "success"); + testFrameSwitching(); + })); + +}, "frame switching API"); diff --git a/test/module/webpage/https-bad-cert.js b/test/module/webpage/https-bad-cert.js new file mode 100644 index 0000000..3c616aa --- /dev/null +++ b/test/module/webpage/https-bad-cert.js @@ -0,0 +1,16 @@ +//! no-snakeoil +async_test(function () { + // This loads the same page as https-good-cert.js, but does not + // tell PhantomJS to trust the snakeoil certificate that the test + // HTTPS server uses, so it should fail. + + var page = require('webpage').create(); + var url = TEST_HTTPS_BASE; + page.onResourceError = this.step_func(function (err) { + assert_equals(err.url, url); + assert_equals(err.errorString, "SSL handshake failed"); + }); + page.open(url, this.step_func_done(function (status) { + assert_not_equals(status, "success"); + })); +}, "should fail to load an HTTPS webpage with a self-signed certificate"); diff --git a/test/module/webpage/https-good-cert.js b/test/module/webpage/https-good-cert.js new file mode 100644 index 0000000..14fc8f1 --- /dev/null +++ b/test/module/webpage/https-good-cert.js @@ -0,0 +1,12 @@ +async_test(function () { + // This loads the same page as https-bad-cert.js; by default the + // test suite tells PhantomJS to trust the snakeoil certificate + // that the test HTTPS server uses, so it should succeed. + + var page = require('webpage').create(); + var url = TEST_HTTPS_BASE; + page.onResourceError = this.unreached_func(); + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + })); +}, "loading an HTTPS webpage"); diff --git a/test/module/webpage/includejs.js b/test/module/webpage/includejs.js new file mode 100644 index 0000000..df3d922 --- /dev/null +++ b/test/module/webpage/includejs.js @@ -0,0 +1,42 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + page.open(TEST_HTTP_BASE + 'includejs1.html', + this.step_func(function (status) { + assert_equals(status, 'success'); + page.includeJs(TEST_HTTP_BASE + 'includejs.js', + this.step_func_done(function () { + var title = page.evaluate('getTitle'); + assert_equals(title, 'i am includejs one'); + })); + })); + +}, "including JS in a page"); + +async_test(function () { + var page = webpage.create(); + var already = false; + page.open(TEST_HTTP_BASE + 'includejs1.html', + this.step_func(function (status) { + assert_equals(status, 'success'); + page.includeJs(TEST_HTTP_BASE + 'includejs.js', + this.step_func(function () { + assert_is_false(already); + already = true; + var title = page.evaluate('getTitle'); + assert_equals(title, 'i am includejs one'); + page.open(TEST_HTTP_BASE + 'includejs2.html', + this.step_func(function (status) { + assert_equals(status, 'success'); + page.includeJs(TEST_HTTP_BASE + 'includejs.js', + this.step_func_done(function () { + assert_is_true(already); + var title = page.evaluate('getTitle'); + assert_equals(title, 'i am includejs two'); + })); + })); + })); + })); + +}, "after-inclusion callbacks should fire only once"); diff --git a/test/module/webpage/keydown-event.js b/test/module/webpage/keydown-event.js new file mode 100644 index 0000000..da0e74b --- /dev/null +++ b/test/module/webpage/keydown-event.js @@ -0,0 +1,20 @@ +test(function () { + var webpage = require('webpage'); + + var page = webpage.create(); + + page.evaluate(function() { + window.addEventListener('keydown', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('keydown', page.event.key.A); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].which, page.event.key.A); +}, "key-down events"); diff --git a/test/module/webpage/keypress-event.js b/test/module/webpage/keypress-event.js new file mode 100644 index 0000000..2272388 --- /dev/null +++ b/test/module/webpage/keypress-event.js @@ -0,0 +1,65 @@ +test(function () { + var webpage = require('webpage'); + + var page = webpage.create(); + + page.evaluate(function() { + window.addEventListener('keypress', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('keypress', page.event.key.C); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].which, page.event.key.C); + + + // Send keypress events to an input element and observe the effect. + + page.content = ''; + page.evaluate(function() { + document.querySelector('input').focus(); + }); + + function getText() { + return page.evaluate(function() { + return document.querySelector('input').value; + }); + } + + page.sendEvent('keypress', page.event.key.A); + assert_equals(getText(), 'A'); + page.sendEvent('keypress', page.event.key.B); + assert_equals(getText(), 'AB'); + page.sendEvent('keypress', page.event.key.Backspace); + assert_equals(getText(), 'A'); + page.sendEvent('keypress', page.event.key.Backspace); + assert_equals(getText(), ''); + + page.sendEvent('keypress', 'XYZ'); + assert_equals(getText(), 'XYZ'); + + // Special character: A with umlaut + page.sendEvent('keypress', 'ä'); + assert_equals(getText(), 'XYZä'); + + // 0x02000000 is the Shift modifier. + page.sendEvent('keypress', page.event.key.Home, null, null, 0x02000000); + page.sendEvent('keypress', page.event.key.Delete); + assert_equals(getText(), ''); + + // Cut and Paste + // 0x04000000 is the Control modifier. + page.sendEvent('keypress', 'ABCD'); + assert_equals(getText(), 'ABCD'); + page.sendEvent('keypress', page.event.key.Home, null, null, 0x02000000); + page.sendEvent('keypress', 'x', null, null, 0x04000000); + assert_equals(getText(), ''); + page.sendEvent('keypress', 'v', null, null, 0x04000000); + assert_equals(getText(), 'ABCD'); +}, "key press events"); diff --git a/test/module/webpage/keyup-event.js b/test/module/webpage/keyup-event.js new file mode 100644 index 0000000..84e7919 --- /dev/null +++ b/test/module/webpage/keyup-event.js @@ -0,0 +1,20 @@ +test(function () { + var webpage = require('webpage'); + + var page = webpage.create(); + + page.evaluate(function() { + window.addEventListener('keyup', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('keyup', page.event.key.B); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].which, page.event.key.B); +}, "key-up events"); diff --git a/test/module/webpage/loading.js b/test/module/webpage/loading.js new file mode 100644 index 0000000..6af3324 --- /dev/null +++ b/test/module/webpage/loading.js @@ -0,0 +1,21 @@ +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + assert_type_of(page, 'object'); + assert_type_of(page.loading, 'boolean'); + assert_type_of(page.loadingProgress, 'number'); + + assert_is_false(page.loading); + assert_equals(page.loadingProgress, 0); + + page.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(page.loading, false); + assert_equals(page.loadingProgress, 100); + })); + + assert_is_true(page.loading); + assert_greater_than(page.loadingProgress, 0); +}, "page loading progress"); diff --git a/test/module/webpage/local-urls-disabled-iframe.js b/test/module/webpage/local-urls-disabled-iframe.js new file mode 100644 index 0000000..6b676d7 --- /dev/null +++ b/test/module/webpage/local-urls-disabled-iframe.js @@ -0,0 +1,22 @@ +//! phantomjs: --web-security=no --local-url-access=no + +var webpage = require("webpage"); + +async_test(function () { + var page = webpage.create(); + var url = TEST_HTTP_BASE + "iframe.html#file:///nonexistent"; + var rsErrorCalled = false; + + page.onResourceError = this.step_func(function (error) { + rsErrorCalled = true; + assert_equals(error.url, "file:///nonexistent"); + assert_equals(error.errorCode, 301); + assert_equals(error.errorString, 'Protocol "file" is unknown'); + }); + + page.open(url, this.step_func_done(function () { + assert_is_true(rsErrorCalled); + })); + +}, +"doesn't attempt to load a file: URL in an iframe with --local-url-access=no"); diff --git a/test/module/webpage/local-urls-disabled.js b/test/module/webpage/local-urls-disabled.js new file mode 100644 index 0000000..85dd1bd --- /dev/null +++ b/test/module/webpage/local-urls-disabled.js @@ -0,0 +1,21 @@ +//! phantomjs: --local-url-access=no + +var webpage = require("webpage"); + +async_test(function () { + var page = webpage.create(); + var url = "file:///nonexistent"; + var rsErrorCalled = false; + + page.onResourceError = this.step_func(function (error) { + rsErrorCalled = true; + assert_equals(error.url, url); + assert_equals(error.errorCode, 301); + assert_equals(error.errorString, 'Protocol "file" is unknown'); + }); + + page.open(url, this.step_func_done(function () { + assert_is_true(rsErrorCalled); + })); + +}, "doesn't attempt to load a file: URL with --local-url-access=no"); diff --git a/test/module/webpage/local-urls-enabled-iframe.js b/test/module/webpage/local-urls-enabled-iframe.js new file mode 100644 index 0000000..103f67e --- /dev/null +++ b/test/module/webpage/local-urls-enabled-iframe.js @@ -0,0 +1,22 @@ +//! phantomjs: --web-security=no --local-url-access=yes + +var webpage = require("webpage"); + +async_test(function () { + var page = webpage.create(); + var url = TEST_HTTP_BASE + "iframe.html#file:///nonexistent"; + var rsErrorCalled = false; + + page.onResourceError = this.step_func(function (error) { + rsErrorCalled = true; + assert_equals(error.url, "file:///nonexistent"); + assert_equals(error.errorCode, 203); + assert_regexp_match(error.errorString, + /^Error opening\b.*?\bnonexistent:/); + }); + + page.open(url, this.step_func_done(function () { + assert_is_true(rsErrorCalled); + })); + +}, "attempts to load a file: URL in an iframe with --local-url-access=yes"); diff --git a/test/module/webpage/local-urls-enabled.js b/test/module/webpage/local-urls-enabled.js new file mode 100644 index 0000000..7555533 --- /dev/null +++ b/test/module/webpage/local-urls-enabled.js @@ -0,0 +1,22 @@ +//! phantomjs: --local-url-access=yes + +var webpage = require("webpage"); + +async_test(function () { + var page = webpage.create(); + var url = "file:///nonexistent"; + var rsErrorCalled = false; + + page.onResourceError = this.step_func(function (error) { + rsErrorCalled = true; + assert_equals(error.url, url); + assert_equals(error.errorCode, 203); + assert_regexp_match(error.errorString, + /^Error opening\b.*?\bnonexistent:/); + }); + + page.open(url, this.step_func_done(function () { + assert_is_true(rsErrorCalled); + })); + +}, "attempts to load a file: URL with --local-url-access=yes"); diff --git a/test/module/webpage/long-running-javascript.js b/test/module/webpage/long-running-javascript.js new file mode 100644 index 0000000..e8ce46a --- /dev/null +++ b/test/module/webpage/long-running-javascript.js @@ -0,0 +1,18 @@ +async_test(function () { + var page = require('webpage').create(); + + page.onLongRunningScript = this.step_func_done(function () { + page.stopJavaScript(); + }); + + page.open(TEST_HTTP_BASE + "js-infinite-loop.html", + this.step_func(function (s) { + assert_equals(s, "success"); + })); + +}, "page.onLongRunningScript can interrupt scripts", { + skip: true // https://github.com/ariya/phantomjs/issues/13490 + // The underlying WebKit feature is so broken that an + // infinite loop in a _page_ script prevents timeouts + // from firing in the _controller_! +}); diff --git a/test/module/webpage/modify-header.js b/test/module/webpage/modify-header.js new file mode 100644 index 0000000..b7ba781 --- /dev/null +++ b/test/module/webpage/modify-header.js @@ -0,0 +1,27 @@ +async_test(function () { + var webpage = require('webpage'); + + // NOTE: HTTP header names are case-insensitive. Our test server + // returns the name in lowercase. + + var page = webpage.create(); + assert_type_of(page.customHeaders, 'object'); + assert_deep_equals(page.customHeaders, {}); + + page.customHeaders = { 'CustomHeader': 'CustomValue' }; + + page.onResourceRequested = this.step_func(function(requestData, request) { + assert_type_of(request.setHeader, 'function'); + request.setHeader('CustomHeader', 'ModifiedCustomValue'); + }); + + page.open(TEST_HTTP_BASE + 'echo', this.step_func_done(function (status) { + var json, headers; + assert_equals(status, 'success'); + json = JSON.parse(page.plainText); + headers = json.headers; + assert_own_property(headers, 'customheader'); + assert_equals(headers.customheader, 'ModifiedCustomValue'); + })); + +}, "modifying HTTP headers"); diff --git a/test/module/webpage/mouseclick-event.js b/test/module/webpage/mouseclick-event.js new file mode 100644 index 0000000..78ed621 --- /dev/null +++ b/test/module/webpage/mouseclick-event.js @@ -0,0 +1,38 @@ +test(function () { + var page = require('webpage').create(); + + page.evaluate(function() { + window.addEventListener('mousedown', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.mousedown = event; + }, false); + window.addEventListener('mouseup', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.mouseup = event; + }, false); + }); + page.sendEvent('click', 42, 217); + + var event = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(event.mouseup.clientX, 42); + assert_equals(event.mouseup.clientY, 217); + assert_equals(event.mousedown.clientX, 42); + assert_equals(event.mousedown.clientY, 217); + + // click with modifier key + page.evaluate(function() { + window.addEventListener('click', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.click = event; + }, false); + }); + page.sendEvent('click', 100, 100, 'left', page.event.modifier.shift); + + var event = page.evaluate(function() { + return window.loggedEvent.click; + }); + assert_is_true(event.shiftKey); + +}, "mouse click events"); diff --git a/test/module/webpage/mousedoubleclick-event.js b/test/module/webpage/mousedoubleclick-event.js new file mode 100644 index 0000000..3ef0ee0 --- /dev/null +++ b/test/module/webpage/mousedoubleclick-event.js @@ -0,0 +1,30 @@ +test(function () { + var page = require('webpage').create(); + + page.content = ''; + var point = page.evaluate(function () { + var el = document.querySelector('input'); + var rect = el.getBoundingClientRect(); + return { x: rect.left + Math.floor(rect.width / 2), y: rect.top + (rect.height / 2) }; + }); + page.sendEvent('doubleclick', point.x, point.y); + + var text = page.evaluate(function () { + return document.querySelector('input').value; + }); + assert_equals(text, "doubleclicked"); + + // click with modifier key + page.evaluate(function() { + window.addEventListener('dblclick', function(event) { + window.loggedEvent = window.loggedEvent || {}; + window.loggedEvent.dblclick = event; + }, false); + }); + page.sendEvent('doubleclick', 100, 100, 'left', page.event.modifier.shift); + + var event = page.evaluate(function() { + return window.loggedEvent.dblclick; + }); + assert_is_true(event.shiftKey); +}, "mouse double-click events"); diff --git a/test/module/webpage/mousedown-event.js b/test/module/webpage/mousedown-event.js new file mode 100644 index 0000000..1e9ab86 --- /dev/null +++ b/test/module/webpage/mousedown-event.js @@ -0,0 +1,26 @@ +test(function () { + var page = require('webpage').create(); + + page.evaluate(function() { + window.addEventListener('mousedown', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('mousedown', 42, 217); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].clientX, 42); + assert_equals(loggedEvent[0].clientY, 217); + + page.sendEvent('mousedown', 100, 100, 'left', page.event.modifier.shift); + loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(loggedEvent.length, 2); + assert_is_true(loggedEvent[1].shiftKey); + +}, "mouse-down events"); diff --git a/test/module/webpage/mousemove-event.js b/test/module/webpage/mousemove-event.js new file mode 100644 index 0000000..e70a785 --- /dev/null +++ b/test/module/webpage/mousemove-event.js @@ -0,0 +1,18 @@ +test(function () { + var page = require('webpage').create(); + + page.evaluate(function() { + window.addEventListener('mousemove', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('mousemove', 14, 3); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].clientX, 14); + assert_equals(loggedEvent[0].clientY, 3); +}, "mouse-move events"); diff --git a/test/module/webpage/mouseup-event.js b/test/module/webpage/mouseup-event.js new file mode 100644 index 0000000..7a7acca --- /dev/null +++ b/test/module/webpage/mouseup-event.js @@ -0,0 +1,26 @@ +test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + page.evaluate(function() { + window.addEventListener('mouseup', function(event) { + window.loggedEvent = window.loggedEvent || []; + window.loggedEvent.push(event); + }, false); + }); + + page.sendEvent('mouseup', 42, 217); + var loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(loggedEvent.length, 1); + assert_equals(loggedEvent[0].clientX, 42); + assert_equals(loggedEvent[0].clientY, 217); + + page.sendEvent('mouseup', 100, 100, 'left', page.event.modifier.shift); + loggedEvent = page.evaluate(function() { + return window.loggedEvent; + }); + assert_equals(loggedEvent.length, 2); + assert_is_true(loggedEvent[1].shiftKey); +}, "mouse-up events"); diff --git a/test/module/webpage/navigation.js b/test/module/webpage/navigation.js new file mode 100644 index 0000000..d471139 --- /dev/null +++ b/test/module/webpage/navigation.js @@ -0,0 +1,30 @@ +async_test(function () { + var page = require("webpage").create(); + var url1 = TEST_HTTP_BASE + "navigation/index.html"; + var url2 = TEST_HTTP_BASE + "navigation/dest.html"; + + var onLoadFinished1 = this.step_func(function (status) { + assert_equals(status, "success"); + assert_equals(page.url, url1); + assert_equals(page.evaluate(function () { + return document.body.innerHTML; + }), "INDEX\n"); + + page.onLoadFinished = onLoadFinished2; + page.evaluate(function() { + window.location = "dest.html"; + }); + }); + + var onLoadFinished2 = this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(page.url, url2); + assert_equals(page.evaluate(function () { + return document.body.innerHTML; + }), "DEST\n"); + }); + + page.onLoadFinished = onLoadFinished1; + page.open(url1); + +}, "navigating to a relative URL using window.location"); diff --git a/test/module/webpage/no-plugin.js b/test/module/webpage/no-plugin.js new file mode 100644 index 0000000..1e563f2 --- /dev/null +++ b/test/module/webpage/no-plugin.js @@ -0,0 +1,19 @@ +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + var pluginLength = page.evaluate(function() { + return window.navigator.plugins.length; + }); + assert_equals(pluginLength, 0); + + page.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + var pluginLength = page.evaluate(function() { + return window.navigator.plugins.length; + }); + assert_equals(pluginLength, 0); + })); + +}, "window.navigator.plugins is empty"); diff --git a/test/module/webpage/object.js b/test/module/webpage/object.js new file mode 100644 index 0000000..8302c37 --- /dev/null +++ b/test/module/webpage/object.js @@ -0,0 +1,76 @@ +var webpage = require("webpage"); +var page = webpage.create(); + +test(function () { + assert_equals(webpage.create, WebPage); +}, "require('webpage').create === global WebPage"); + +test(function () { + assert_type_of(page, 'object'); + assert_not_equals(page, null); + + assert_equals(page.objectName, 'WebPage'); + assert_deep_equals(page.paperSize, {}); + + assert_not_equals(page.settings, null); + assert_not_equals(page.settings, {}); + + assert_type_of(page.canGoForward, 'boolean'); + assert_type_of(page.canGoBack, 'boolean'); + assert_type_of(page.clipRect, 'object'); + assert_type_of(page.content, 'string'); + assert_type_of(page.cookieJar, 'object'); + assert_type_of(page.cookies, 'object'); + assert_type_of(page.customHeaders, 'object'); + assert_type_of(page.event, 'object'); + assert_type_of(page.libraryPath, 'string'); + assert_type_of(page.loading, 'boolean'); + assert_type_of(page.loadingProgress, 'number'); + assert_type_of(page.navigationLocked, 'boolean'); + assert_type_of(page.offlineStoragePath, 'string'); + assert_type_of(page.offlineStorageQuota, 'number'); + assert_type_of(page.paperSize, 'object'); + assert_type_of(page.plainText, 'string'); + assert_type_of(page.scrollPosition, 'object'); + assert_type_of(page.settings, 'object'); + assert_type_of(page.title, 'string'); + assert_type_of(page.url, 'string'); + assert_type_of(page.frameUrl, 'string'); + assert_type_of(page.viewportSize, 'object'); + assert_type_of(page.windowName, 'string'); + assert_type_of(page.zoomFactor, 'number'); + +}, "page object properties"); + +test(function () { + assert_type_of(page.childFramesCount, 'function'); + assert_type_of(page.childFramesName, 'function'); + assert_type_of(page.clearMemoryCache, 'function'); + assert_type_of(page.close, 'function'); + assert_type_of(page.currentFrameName, 'function'); + assert_type_of(page.deleteLater, 'function'); + assert_type_of(page.destroyed, 'function'); + assert_type_of(page.evaluate, 'function'); + assert_type_of(page.initialized, 'function'); + assert_type_of(page.injectJs, 'function'); + assert_type_of(page.javaScriptAlertSent, 'function'); + assert_type_of(page.javaScriptConsoleMessageSent, 'function'); + assert_type_of(page.loadFinished, 'function'); + assert_type_of(page.loadStarted, 'function'); + assert_type_of(page.openUrl, 'function'); + assert_type_of(page.release, 'function'); + assert_type_of(page.render, 'function'); + assert_type_of(page.resourceError, 'function'); + assert_type_of(page.resourceReceived, 'function'); + assert_type_of(page.resourceRequested, 'function'); + assert_type_of(page.uploadFile, 'function'); + assert_type_of(page.sendEvent, 'function'); + assert_type_of(page.setContent, 'function'); + assert_type_of(page.switchToChildFrame, 'function'); + assert_type_of(page.switchToMainFrame, 'function'); + assert_type_of(page.switchToParentFrame, 'function'); + + assert_type_of(page.addCookie, 'function'); + assert_type_of(page.deleteCookie, 'function'); + assert_type_of(page.clearCookies, 'function'); +}, "page object methods"); diff --git a/test/module/webpage/on-confirm.js b/test/module/webpage/on-confirm.js new file mode 100644 index 0000000..f4f6e31 --- /dev/null +++ b/test/module/webpage/on-confirm.js @@ -0,0 +1,33 @@ +test(function () { + var page = require('webpage').create(); + + var msg = "message body", + result, + expected = true; + + assert_equals(page.onConfirm, undefined); + + var onConfirmTrue = function(msg) { + return true; + }; + page.onConfirm = onConfirmTrue; + assert_equals(page.onConfirm, onConfirmTrue); + + result = page.evaluate(function(m) { + return window.confirm(m); + }, msg); + + assert_equals(result, expected); + + var onConfirmFunc = function() { return !!"y"; }; + page.onConfirm = onConfirmFunc; + assert_equals(page.onConfirm, onConfirmFunc); + assert_not_equals(page.onConfirm, onConfirmTrue); + + page.onConfirm = null; + // Will only allow setting to a function value, so setting it to `null` returns `undefined` + assert_equals(page.onConfirm, undefined); + page.onConfirm = undefined; + assert_equals(page.onConfirm, undefined); + +}, "page.onConfirm"); diff --git a/test/module/webpage/on-error.js b/test/module/webpage/on-error.js new file mode 100644 index 0000000..683c061 --- /dev/null +++ b/test/module/webpage/on-error.js @@ -0,0 +1,109 @@ +var webpage = require('webpage'); + +test(function () { + var page = webpage.create(); + assert_not_equals(page.onError, undefined); + + var onErrorFunc1 = function() { return !"x"; }; + page.onError = onErrorFunc1; + assert_equals(page.onError, onErrorFunc1); + + var onErrorFunc2 = function() { return !!"y"; }; + page.onError = onErrorFunc2; + assert_equals(page.onError, onErrorFunc2); + assert_not_equals(page.onError, onErrorFunc1); + + page.onError = null; + // Will only allow setting to a function value, so setting it to `null` returns `undefined` + assert_equals(page.onError, undefined); + page.onError = undefined; + assert_equals(page.onError, undefined); +}, "setting and clearing page.onError"); + +test(function () { + var page = webpage.create(); + var lastError = null; + page.onError = function(message) { lastError = message; }; + + page.evaluate(function() { referenceError2(); }); + assert_equals(lastError, "ReferenceError: Can't find variable: referenceError2"); + + page.evaluate(function() { throw "foo"; }); + assert_equals(lastError, "foo"); + + page.evaluate(function() { throw Error("foo"); }); + assert_equals(lastError, "Error: foo"); +}, "basic error reporting"); + +async_test(function () { + var page = webpage.create(); + var lastError = null; + page.onError = this.step_func_done(function(message) { + assert_equals(message, "ReferenceError: Can't find variable: referenceError"); + }); + + page.evaluate(function() { + setTimeout(function() { referenceError(); }, 0); + }); + +}, "error reporting from async events"); + +test(function () { + var page = webpage.create(); + var hadError = false; + page.onError = function() { hadError = true; }; + page.evaluate(function() { + window.caughtError = false; + + try { + referenceError(); + } catch(e) { + window.caughtError = true; + } + }); + + assert_equals(hadError, false); + assert_is_true(page.evaluate(function() { return window.caughtError; })); +}, "should not report errors that were caught"); + +function check_stack(message, stack) { + assert_equals(message, + "ReferenceError: Can't find variable: referenceError"); + + if (typeof stack === "string") { + var lines = stack.split("\n"); + assert_regexp_match(lines[0], RegExp("^bar@.*?"+helperBase+":7:23$")); + assert_regexp_match(lines[1], RegExp("^foo@.*?"+helperBase+":3:17$")); + } else { + assert_regexp_match(stack[0].file, RegExp(helperBase)); + assert_equals(stack[0].line, 7); + assert_equals(stack[0]["function"], "bar"); + + assert_regexp_match(stack[1].file, RegExp(helperBase)); + assert_equals(stack[1].line, 3); + assert_equals(stack[1]["function"], "foo"); + } +} + +var helperBase = "error-helper.js"; +var helperFile = "../../fixtures/" + helperBase; +assert_is_true(phantom.injectJs(helperFile)); + +test(function () { + try { + ErrorHelper.foo(); + } catch (e) { + check_stack(e.toString(), e.stack); + } +}, "stack trace accuracy (controller script)"); + +async_test(function () { + var page = webpage.create(); + page.libraryPath = phantom.libraryPath; + assert_is_true(page.injectJs(helperFile)); + + page.onError = this.step_func_done(check_stack); + page.evaluate(function () { + setTimeout(function () { ErrorHelper.foo(); }, 0); + }); +}, "stack trace accuracy (webpage script)"); diff --git a/test/module/webpage/on-initialized.js b/test/module/webpage/on-initialized.js new file mode 100644 index 0000000..3f8c735 --- /dev/null +++ b/test/module/webpage/on-initialized.js @@ -0,0 +1,21 @@ +test(function () { + var page = require('webpage').create(); + + assert_equals(page.onInitialized, undefined); + + var onInitialized1 = function() { var x = "x"; }; + page.onInitialized = onInitialized1; + assert_equals(page.onInitialized, onInitialized1); + + var onInitialized2 = function() { var y = "y"; }; + page.onInitialized = onInitialized2; + assert_equals(page.onInitialized, onInitialized2); + assert_not_equals(page.onInitialized, onInitialized1); + + page.onInitialized = null; + // Will only allow setting to a function value, so setting it to `null` returns `undefined` + assert_equals(page.onInitialized, undefined); + + page.onInitialized = undefined; + assert_equals(page.onInitialized, undefined); +}, "page.onInitialized"); diff --git a/test/module/webpage/open.js b/test/module/webpage/open.js new file mode 100644 index 0000000..513b3fa --- /dev/null +++ b/test/module/webpage/open.js @@ -0,0 +1,53 @@ +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + assert_type_of(page, 'object'); + + page.onResourceReceived = this.step_func(function (resource) { + assert_equals(resource.status, 200); + }); + page.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_type_of(page.title, 'string'); + assert_equals(page.title, 'Hello'); + assert_type_of(page.plainText, 'string'); + assert_equals(page.plainText, 'Hello, world!'); + })); +}, "opening a webpage"); + +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + // both onResourceReceived and onResourceError should be called + page.onResourceReceived = this.step_func(function (resource) { + assert_equals(resource.status, 401); + }); + page.onResourceError = this.step_func(function (err) { + assert_equals(err.errorString, "Operation canceled"); + }); + + page.open(TEST_HTTP_BASE + 'status?status=401' + + '&WWW-Authenticate=Basic%20realm%3D%22PhantomJS%20test%22', + this.step_func_done(function (status) { + assert_equals(status, 'fail'); + })); + +}, "proper handling of HTTP error responses"); + +async_test(function () { + var webpage = require('webpage'); + var page = webpage.create(); + + page.settings.resourceTimeout = 1; + + // This is all you have to do to assert that a hook does get called. + page.onResourceTimeout = this.step_func(function(){}); + + page.open(TEST_HTTP_BASE + "delay?5", + this.step_func_done(function (s) { + assert_not_equals(s, "success"); + })); + +}, "onResourceTimeout fires after resourceTimeout ms"); diff --git a/test/module/webpage/postdata.js b/test/module/webpage/postdata.js new file mode 100644 index 0000000..68f27a9 --- /dev/null +++ b/test/module/webpage/postdata.js @@ -0,0 +1,46 @@ +function validate_echo_response (status, page, postdata) { + assert_equals(status, 'success'); + + var desc = JSON.parse(page.plainText); + assert_equals(desc.command, "POST"); + assert_equals(desc.postdata, postdata); + assert_equals(desc.headers['content-type'], + 'application/x-www-form-urlencoded'); +} + +async_test(function () { + + var utfString = '안녕'; + var openOptions = { + operation: 'POST', + data: utfString, + encoding: 'utf8' + }; + var pageOptions = { + onLoadFinished: this.step_func_done(function(status) { + validate_echo_response(status, page, utfString); + }) + }; + var page = new WebPage(pageOptions); + page.openUrl(TEST_HTTP_BASE + "echo", openOptions, {}); + + +}, "processing request body for POST"); + +async_test(function () { + + var postdata = "ab=cd"; + var pageOptions = { + onResourceRequested: this.step_func(function (request) { + assert_equals(request.postData, postdata); + }), + onLoadFinished: this.step_func_done(function (status) { + validate_echo_response(status, page, postdata); + }) + }; + + var page = new WebPage(pageOptions); + page.open(TEST_HTTP_BASE + "echo", 'post', postdata); + + +}, "POST data is available in onResourceRequested"); diff --git a/test/module/webpage/prompt.js b/test/module/webpage/prompt.js new file mode 100644 index 0000000..d1f099e --- /dev/null +++ b/test/module/webpage/prompt.js @@ -0,0 +1,16 @@ +test(function () { + var page = require('webpage').create(); + + var msg = "message", + value = "value", + result, + expected = "extra-value"; + page.onPrompt = function(msg, value) { + return "extra-"+value; + }; + result = page.evaluate(function(m, v) { + return window.prompt(m, v); + }, msg, value); + + assert_equals(result, expected); +}, "page.onPrompt"); diff --git a/test/module/webpage/remove-header.js b/test/module/webpage/remove-header.js new file mode 100644 index 0000000..900a870 --- /dev/null +++ b/test/module/webpage/remove-header.js @@ -0,0 +1,26 @@ +var webpage = require('webpage'); + +// NOTE: HTTP header names are case-insensitive. Our test server +// returns the name in lowercase. +async_test(function () { + var page = webpage.create(); + assert_type_of(page.customHeaders, 'object'); + assert_deep_equals(page.customHeaders, {}); + + page.customHeaders = { 'CustomHeader': 'ModifiedCustomValue' }; + + page.onResourceRequested = this.step_func(function(requestData, request) { + assert_type_of(request.setHeader, 'function'); + request.setHeader('CustomHeader', null); + }); + + page.open(TEST_HTTP_BASE + 'echo', + this.step_func_done(function (status) { + var json, headers; + assert_equals(status, 'success'); + json = JSON.parse(page.plainText); + headers = json.headers; + assert_no_property(headers, 'customheader'); + assert_no_property(headers, 'CustomHeader'); + })); +}); diff --git a/test/module/webpage/render.js b/test/module/webpage/render.js new file mode 100644 index 0000000..230192c --- /dev/null +++ b/test/module/webpage/render.js @@ -0,0 +1,67 @@ +var fs = require("fs"); +var system = require("system"); +var webpage = require("webpage"); +var renders = require("./renders"); + +function clean_pdf(data) { + // FIXME: This is not nearly enough normalization. + data = data.replace(/\/(Title|Creator|Producer|CreationDate) \([^\n]*\)/g, "/$1 ()"); + data = data.replace(/\nxref\n[0-9 nf\n]+trailer\b/, "\ntrailer"); + data = data.replace(/\nstartxref\n[0-9]+\n%%EOF\n/, "\n"); + return data; +} + +function render_test(format, option) { + var opt = option || {}; + var scratch = "temp_render"; + if (!opt.format) { + scratch += "."; + scratch += format; + } + var expect_content = renders.get(format, opt.quality || ""); + var p = webpage.create(); + + p.paperSize = { width: '300px', height: '300px', border: '0px' }; + p.clipRect = { top: 0, left: 0, width: 300, height: 300}; + p.viewportSize = { width: 300, height: 300}; + + p.open(TEST_HTTP_BASE + "render/", this.step_func_done(function (status) { + p.render(scratch, opt); + this.add_cleanup(function () { fs.remove(scratch); }); + var content = fs.read(scratch, "b"); + + // expected variation in PDF output + if (format === "pdf") { + content = clean_pdf(content); + expect_content = clean_pdf(expect_content); + } + + // Don't dump entire images to the log on failure. + assert_is_true(content === expect_content); + })); +} + +[ + ["PDF", "pdf", {}], + ["PDF (format option)", "pdf", {format: "pdf"}], + ["PNG", "png", {}], + ["PNG (format option)", "png", {format: "png"}], + ["JPEG", "jpg", {}], + ["JPEG (format option)", "jpg", {format: "jpg"}], + ["JPEG (quality option)", "jpg", {quality: 50}], + ["JPEG (format and quality options)", "jpg", {format: "jpg", quality: 50}], +] +.forEach(function (arr) { + var label = arr[0]; + var format = arr[1]; + var opt = arr[2]; + var props = {}; + + // All tests fail on Linux. All tests except JPG fail on Mac. + // Currently unknown which tests fail on Windows. + if (format !== "jpg" || system.os.name !== "mac") + props.expected_fail = true; + + async_test(function () { render_test.call(this, format, opt); }, + label, props); +}); diff --git a/test/module/webpage/renders/index.js b/test/module/webpage/renders/index.js new file mode 100644 index 0000000..47e83b6 --- /dev/null +++ b/test/module/webpage/renders/index.js @@ -0,0 +1,7 @@ +var fs = require("fs"); + +exports.get = function get(format, quality) { + var expect_file = fs.join( + module.dirname, "test" + quality + "." + format); + return fs.read(expect_file, "b"); +}; diff --git a/test/module/webpage/renders/test.jpg b/test/module/webpage/renders/test.jpg new file mode 100644 index 0000000..b597dba Binary files /dev/null and b/test/module/webpage/renders/test.jpg differ diff --git a/test/module/webpage/renders/test.pdf b/test/module/webpage/renders/test.pdf new file mode 100644 index 0000000..b1beecd Binary files /dev/null and b/test/module/webpage/renders/test.pdf differ diff --git a/test/module/webpage/renders/test.png b/test/module/webpage/renders/test.png new file mode 100644 index 0000000..7932eeb Binary files /dev/null and b/test/module/webpage/renders/test.png differ diff --git a/test/module/webpage/renders/test50.jpg b/test/module/webpage/renders/test50.jpg new file mode 100644 index 0000000..a3e0442 Binary files /dev/null and b/test/module/webpage/renders/test50.jpg differ diff --git a/test/module/webpage/repaint-requested.js b/test/module/webpage/repaint-requested.js new file mode 100644 index 0000000..dfd3eec --- /dev/null +++ b/test/module/webpage/repaint-requested.js @@ -0,0 +1,19 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + var requestCount = 0; + + page.onRepaintRequested = this.step_func(function(x, y, w, h) { + if ((w > 0) && (h > 0)) { + ++requestCount; + } + }); + + page.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_greater_than(requestCount, 0); + })); + +}, "onRepaintRequested should be called at least once for each page load"); diff --git a/test/module/webpage/resource-received-error.js b/test/module/webpage/resource-received-error.js new file mode 100644 index 0000000..ce8ecb9 --- /dev/null +++ b/test/module/webpage/resource-received-error.js @@ -0,0 +1,32 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + var url = TEST_HTTP_BASE + 'status?400'; + var startStage = 0; + var endStage = 0; + var errors = 0; + + page.onResourceReceived = this.step_func(function (resource) { + assert_equals(resource.url, url); + if (resource.stage === 'start') { + ++startStage; + } + if (resource.stage === 'end') { + ++endStage; + } + }); + page.onResourceError = this.step_func(function (error) { + assert_equals(error.url, url); + assert_equals(error.status, 400); + ++errors; + }); + + page.open(url, this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(startStage, 1); + assert_equals(endStage, 1); + assert_equals(errors, 1); + })); + +}, "onResourceReceived should still be called for failed requests"); diff --git a/test/module/webpage/resource-request-error.js b/test/module/webpage/resource-request-error.js new file mode 100644 index 0000000..4123e66 --- /dev/null +++ b/test/module/webpage/resource-request-error.js @@ -0,0 +1,26 @@ +var webpage = require('webpage'); + +async_test(function () { + var page = webpage.create(); + var resourceErrors = 0; + + page.onResourceError = this.step_func(function(err) { + ++resourceErrors; + + assert_equals(err.status, 404); + assert_equals(err.statusText, 'File not found'); + assert_equals(err.url, TEST_HTTP_BASE + 'notExist.png'); + assert_equals(err.errorCode, 203); + assert_regexp_match(err.errorString, + /Error downloading http:\/\/localhost:[0-9]+\/notExist\.png/); + assert_regexp_match(err.errorString, + /server replied: File not found/); + }); + + page.open(TEST_HTTP_BASE + 'missing-img.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(resourceErrors, 1); + })); + +}, "resourceError basic functionality"); diff --git a/test/module/webpage/scroll-position.js b/test/module/webpage/scroll-position.js new file mode 100644 index 0000000..d1311e4 --- /dev/null +++ b/test/module/webpage/scroll-position.js @@ -0,0 +1,17 @@ +var webpage = require('webpage'); + +test(function () { + var defaultPage = webpage.create(); + assert_deep_equals(defaultPage.scrollPosition, {left:0,top:0}); +}, "default scroll position"); + +test(function () { + var options = { + scrollPosition: { + left: 1, + top: 2 + } + }; + var customPage = webpage.create(options); + assert_deep_equals(customPage.scrollPosition, options.scrollPosition); +}, "custom scroll position"); diff --git a/test/module/webpage/set-content.js b/test/module/webpage/set-content.js new file mode 100644 index 0000000..937407c --- /dev/null +++ b/test/module/webpage/set-content.js @@ -0,0 +1,19 @@ +var webpage = require('webpage'); + +test(function () { + var page = webpage.create(); + var expectedContent = '
Test div
'; + var expectedLocation = 'http://www.phantomjs.org/'; + page.setContent(expectedContent, expectedLocation); + + var actualContent = page.evaluate(function() { + return document.documentElement.textContent; + }); + assert_equals(actualContent, 'Test div'); + + var actualLocation = page.evaluate(function() { + return window.location.href; + }); + assert_equals(actualLocation, expectedLocation); + +}, "manually set page content and location"); diff --git a/test/module/webpage/subwindows.js b/test/module/webpage/subwindows.js new file mode 100644 index 0000000..49c5c53 --- /dev/null +++ b/test/module/webpage/subwindows.js @@ -0,0 +1,129 @@ +async_test(function () { + var test = this; + var top = require('webpage').create(); + var pages_created = 0; + var expect_to_close = null; + var after_close = null; + + top.onPageCreated = function (page) { + pages_created++; + page.onClosing = test.step_func(function (page) { + assert_equals(page.windowName, expect_to_close); + setTimeout(after_close, 0); + }); + if (pages_created === 3) { + setTimeout(after_open_3, 0); + } + }; + + var after_open_3 = test.step_func(function () { + assert_equals(top.pages.length, 3); + assert_deep_equals(top.pagesWindowName, ["A", "B", "C"]); + + after_close = after_close_1; + expect_to_close = "A"; + top.evaluate(function () { window.wA.close(); }); + }); + + var after_close_1 = test.step_func(function () { + assert_equals(top.pages.length, 2); + assert_deep_equals(top.pagesWindowName, ["B", "C"]); + + + var pageB = top.getPage("B"); + assert_not_equals(pageB, null); + + after_close = after_close_2; + expect_to_close = "B"; + pageB.close(); + }); + + var after_close_2 = test.step_func(function () { + assert_equals(top.pages.length, 1); + assert_deep_equals(top.pagesWindowName, ["C"]); + + // Must close C as well, because its onclosing hook is a step + // function that hasn't run yet. + after_close = test.step_func_done(); + expect_to_close = "C"; + top.close(); + }); + + top.evaluate(function () { + var w = window; + w.wA = w.open("data:text/html,%3Ctitle%3Epage%20A%3C/title%3E", "A"); + w.wB = w.open("data:text/html,%3Ctitle%3Epage%20B%3C/title%3E", "B"); + w.wC = w.open("data:text/html,%3Ctitle%3Epage%20C%3C/title%3E", "C"); + }); + +}, "pages and pagesWindowName arrays; onPageCreated and onClosing hooks"); + +async_test(function () { + var test = this; + var pages_opened = 1, pages_closed = 0; + var top = require("webpage").create(); + + var onPageCreated = test.step_func(function onPageCreated(page) { + pages_opened++; + page.onPageCreated = onPageCreated; + page.onClosing = onClosing; + if (pages_opened === 4) { + setTimeout(after_open_4, 0); + } + }); + var onClosing = test.step_func(function onClosing(page) { + pages_closed++; + if (pages_opened === pages_closed) { + test.done(); + } + }); + + // This can't be inlined into onPageCreated because + // pagesWindowName is not quite up-to-date when that hook fires. + var after_open_4 = test.step_func(function () { + assert_equals(top.pages.length, 3); + assert_deep_equals(top.pagesWindowName, ["A", "B", "C"]); + top.close(); + }); + + top.onPageCreated = onPageCreated; + top.onClosing = onClosing; + + top.evaluate(function () { + var w = window; + w.wA = w.open("data:text/html,%3Ctitle%3Epage%20A%3C/title%3E", "A"); + w.wB = w.open("data:text/html,%3Ctitle%3Epage%20B%3C/title%3E", "B"); + w.wC = w.open("data:text/html,%3Ctitle%3Epage%20C%3C/title%3E", "C"); + }); + +}, "close subwindows when parent page is closed (default behavior)"); + +async_test(function () { + var test = this; + var pages_opened = 1; + var top = require("webpage").create(); + top.ownsPages = false; + + var onPageCreated = test.step_func(function onPageCreated(page) { + pages_opened++; + page.onPageCreated = onPageCreated; + page.onClosing = test.unreached_func(); + if (pages_opened === 4) { + assert_equals(top.pages.length, 0); + assert_deep_equals(top.pagesWindowName, []); + top.close(); + } + }); + top.onPageCreated = onPageCreated; + + top.onClosing = test.step_func(function onTopClosing(page) { + setTimeout(function () { test.done(); }, 50); + }); + + top.evaluate(function () { + var w = window; + w.wA = w.open("data:text/html,%3Ctitle%3Epage%20A%3C/title%3E", "A"); + w.wB = w.open("data:text/html,%3Ctitle%3Epage%20B%3C/title%3E", "B"); + w.wC = w.open("data:text/html,%3Ctitle%3Epage%20C%3C/title%3E", "C"); + }); +}, "don't close subwindows when parent page is closed (ownsPages=false)"); diff --git a/test/module/webpage/url-encoding.js b/test/module/webpage/url-encoding.js new file mode 100644 index 0000000..86af31b --- /dev/null +++ b/test/module/webpage/url-encoding.js @@ -0,0 +1,133 @@ +var webpage = require('webpage'); + +// Many of the URLs used in this file contain text encoded in +// Shift_JIS, so that they will not round-trip correctly if +// misinterpreted at any point as UTF-8 (and thus, the test will +// fail). See www/url-encoding.py for Unicode equivalents. + +function URL(path) { + return TEST_HTTP_BASE + 'url-encoding?' + path; +} + +async_test(function () { + var p = webpage.create(); + p.open(URL('/'), this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(p.url, URL('/%83y%81[%83W')); + assert_equals(p.plainText, 'PASS'); + })); + +}, "page.url"); + +async_test(function () { + var p = webpage.create(); + p.open(URL('/f'), this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(p.url, URL('/f')); + assert_equals(p.framesCount, 2); + + assert_is_true(p.switchToFrame('a')); + assert_equals(p.frameUrl, URL('/%98g')); + assert_equals(p.framePlainText, 'PASS'); + + assert_is_true(p.switchToParentFrame()); + assert_is_true(p.switchToFrame('b')); + assert_equals(p.frameUrl, URL('/%95s%96%D1%82%C8%98_%91%88')); + assert_equals(p.framePlainText, 'FRAME'); + })); + +}, "page.frameUrl"); + +async_test(function () { + var p = webpage.create(); + var n = 0; + var expectedUrls = [ URL('/'), URL('/%83y%81[%83W') ]; + + p.onNavigationRequested = this.step_func(function (url, ty, will, main) { + assert_equals(url, expectedUrls[n]); + assert_equals(ty, 'Other'); + assert_is_true(will); + assert_is_true(main); + n++; + + if (n === expectedUrls.length) { + p.onNavigationRequested = this.unreached_func(); + } + }); + p.open(URL('/'), this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(n, expectedUrls.length); + assert_equals(p.plainText, 'PASS'); + })); + +}, "arguments to onNavigationRequested"); + +async_test(function () { + var p = webpage.create(); + var n = 0; + var n_req = 0; + var n_recv = 0; + var expectedUrls = [ URL('/r'), URL('/%8F%91') ]; + var receivedUrls = {}; + + p.onResourceRequested = this.step_func(function (req, nr) { + assert_equals(req.url, expectedUrls[n_req]); + n_req++; + if (n_req === expectedUrls.length) { + p.onResourceRequested = this.unreached_func(); + } + }); + + p.onResourceReceived = this.step_func(function (resp) { + // This function may be called more than once per URL. + if (receivedUrls.hasOwnProperty(resp.url)) + return; + receivedUrls[resp.url] = true; + assert_equals(resp.url, expectedUrls[n_recv]); + n_recv++; + }); + + p.open(URL('/r'), this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(n_req, expectedUrls.length); + assert_equals(n_recv, expectedUrls.length); + assert_equals(p.plainText, 'PASS'); + })); + +}, "arguments to onResourceRequested and onResourceReceived"); + +async_test(function () { + var p = webpage.create(); + p.settings.resourceTimeout = 100; + + var n_timeout = 0; + var n_error = 0; + var expectedUrls_timeout = [ URL('/%89i%8Bv') ]; + // the error hook is called for timeouts as well + var expectedUrls_error = [ URL('/%8C%CC%8F%E1'), URL('/%89i%8Bv') ]; + + p.onResourceTimeout = this.step_func(function (req) { + assert_equals(req.url, expectedUrls_timeout[n_timeout]); + n_timeout++; + + if (n_timeout === expectedUrls_timeout.length) { + p.onResourceTimeout = this.unreached_func(); + } + }); + + p.onResourceError = this.step_func(function (err) { + assert_equals(err.url, expectedUrls_error[n_error]); + n_error++; + + if (n_error === expectedUrls_error.length) { + p.onResourceTimeout = this.unreached_func(); + } + }); + + p.open(URL("/re"), this.step_func_done(function (status) { + assert_equals(status, 'success'); + assert_equals(n_timeout, expectedUrls_timeout.length); + assert_equals(n_error, expectedUrls_error.length); + })); + +}, " arguments to onResourceError and onResourceTimeout"); diff --git a/test/module/webpage/user-agent.js b/test/module/webpage/user-agent.js new file mode 100644 index 0000000..1dc1962 --- /dev/null +++ b/test/module/webpage/user-agent.js @@ -0,0 +1,22 @@ +var webpage = require('webpage'); + +async_test(function () { + var ua = 'PHANTOMJS-TEST-USER-AGENT'; + var page = webpage.create({ + settings: { + userAgent: ua + } + }); + + assert_equals(page.settings.userAgent, ua); + + page.open(TEST_HTTP_BASE + 'user-agent.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + var agent = page.evaluate(function() { + return document.getElementById('ua').textContent; + }); + assert_equals(agent, ua); + })); + +}, "load a page with a custom user agent"); diff --git a/test/module/webpage/viewport-size.js b/test/module/webpage/viewport-size.js new file mode 100644 index 0000000..dc2412c --- /dev/null +++ b/test/module/webpage/viewport-size.js @@ -0,0 +1,17 @@ +var webpage = require('webpage'); + +test(function () { + var defaultPage = webpage.create(); + assert_deep_equals(defaultPage.viewportSize, {height:300,width:400}); +}, "default viewport size"); + +test(function () { + var options = { + viewportSize: { + height: 100, + width: 200 + } + }; + var customPage = webpage.create(options); + assert_deep_equals(customPage.viewportSize, options.viewportSize); +}, "custom viewport size"); diff --git a/test/module/webpage/window.js b/test/module/webpage/window.js new file mode 100644 index 0000000..c8ddaaf --- /dev/null +++ b/test/module/webpage/window.js @@ -0,0 +1,4 @@ +test(function () { + assert_own_property(window, 'WebPage'); + assert_type_of(window.WebPage, 'function'); +}, "window.WebPage global property"); diff --git a/test/module/webpage/zoom-factor.js b/test/module/webpage/zoom-factor.js new file mode 100644 index 0000000..ba695ff --- /dev/null +++ b/test/module/webpage/zoom-factor.js @@ -0,0 +1,17 @@ +var webpage = require('webpage'); + +test(function () { + var page = webpage.create(); + assert_equals(page.zoomFactor, 1.0); + + page.zoomFactor = 1.5; + assert_equals(page.zoomFactor, 1.5); + + page.zoomFactor = 2.0; + assert_equals(page.zoomFactor, 2.0); + + page.zoomFactor = 0.5; + assert_equals(page.zoomFactor, 0.5); +}, "page.zoomFactor"); + +// TODO: render using zoomFactor != 1 and check the result diff --git a/test/module/webserver/basics.js b/test/module/webserver/basics.js new file mode 100644 index 0000000..4f124cc --- /dev/null +++ b/test/module/webserver/basics.js @@ -0,0 +1,25 @@ +test(function () { + assert_no_property(window, "WebServer", + "WebServer constructor should not be global"); + + var WebServer = require("webserver").create; + assert_type_of(WebServer, "function"); + +}, "WebServer constructor"); + +test(function () { + var server = require("webserver").create(); + + assert_not_equals(server, null); + assert_type_of(server, "object"); + assert_equals(server.objectName, "WebServer"); + + assert_own_property(server, "port"); + assert_type_of(server.port, "string"); + assert_equals(server.port, ""); + + assert_type_of(server.listenOnPort, "function"); + assert_type_of(server.newRequest, "function"); + assert_type_of(server.close, "function"); + +}, "WebServer object properties"); diff --git a/test/module/webserver/requests.js b/test/module/webserver/requests.js new file mode 100644 index 0000000..04bbe33 --- /dev/null +++ b/test/module/webserver/requests.js @@ -0,0 +1,175 @@ +var server, port, request_cb; +setup(function () { + server = require("webserver").create(); + + // Should be unable to listen on port 1 (FIXME: this might succeed if + // the test suite is being run with root privileges). + assert_is_false(server.listen(1, function () {})); + assert_equals(server.port, ""); + + // Find an unused port in the 1024--32767 range on which to run the + // rest of the tests. The function in "request_cb" will be called + // for each request; it is set appropriately by each test case. + for (var i = 1024; i < 32768; i++) { + if (server.listen(i, function(rq,rs){return request_cb(rq,rs);})) { + assert_equals(server.port, i.toString()); + port = server.port; + return; + } + } + assert_unreached("unable to find a free TCP port for server tests"); +}, + { "test_timeout": 1000 }); + +function arm_check_request (test, expected_postdata, expected_bindata, + expected_mimetype) { + request_cb = test.step_func(function check_request (request, response) { + try { + assert_type_of(request, "object"); + assert_own_property(request, "url"); + assert_own_property(request, "method"); + assert_own_property(request, "httpVersion"); + assert_own_property(request, "headers"); + assert_type_of(request.headers, "object"); + + assert_type_of(response, "object"); + assert_own_property(response, "statusCode"); + assert_own_property(response, "headers"); + assert_type_of(response.setHeaders, "function"); + assert_type_of(response.setHeader, "function"); + assert_type_of(response.header, "function"); + assert_type_of(response.write, "function"); + assert_type_of(response.writeHead, "function"); + + if (expected_postdata !== false) { + assert_equals(request.method, "POST"); + assert_own_property(request, "post"); + if (request.headers["Content-Type"] === + "application/x-www-form-urlencoded") { + assert_own_property(request, "postRaw"); + assert_type_of(request.postRaw, "string"); + assert_type_of(request.post, "object"); + assert_deep_equals(request.post, expected_postdata); + } else { + assert_no_property(request, "postRaw"); + assert_type_of(request.post, "string"); + assert_not_equals(request.post, expected_postdata); + } + } + + response.setHeader("X-Request-URL", request.url); + + if (expected_bindata !== false) { + response.setEncoding("binary"); + response.setHeader("Content-Type", expected_mimetype); + response.write(expected_bindata); + } else { + response.write("request handled"); + } + } finally { + response.close(); + request_cb = test.unreached_func(); + } + }); +} + +async_test(function () { + var page = require("webpage").create(); + var url = "http://localhost:"+port+"/foo/bar.php?asdf=true"; + + arm_check_request(this, false, false); + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(page.plainText, "request handled"); + })); + +}, "basic request handling"); + +async_test(function () { + var page = require("webpage").create(); + var url = "http://localhost:"+port+"/%95s%96%D1%82%C8%98_%91%88"; + var already = false; + + arm_check_request(this, false, false); + page.onResourceReceived = this.step_func(function (resp) { + if (already) return; + already = true; + + var found = false; + resp.headers.forEach(function (hdr) { + if (hdr.name.toLowerCase() === "x-request-url") { + assert_equals(hdr.value, "/%95s%96%D1%82%C8%98_%91%88"); + found = true; + } + }); + assert_is_true(found); + }); + + page.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(page.plainText, "request handled"); + })); + +}, "round-trip of URLs containing encoded non-Unicode text"); + +async_test(function () { + var page = require("webpage").create(); + var url = "http://localhost:"+port+"/foo/bar.txt?asdf=true"; + + arm_check_request(this, + {"answer" : "42", "universe" : "expanding"}, false); + + page.open(url, "post", "universe=expanding&answer=42", + { "Content-Type" : "application/x-www-form-urlencoded" }, + this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(page.plainText, "request handled"); + })); + +}, "handling POST with application/x-www-form-urlencoded data"); + +async_test(function () { + var page = require("webpage").create(); + var url = "http://localhost:"+port+"/foo/bar.txt?asdf=true"; + + arm_check_request(this, + {"answer" : "42", "universe" : "expanding"}, false); + + page.open(url, "post", "universe=expanding&answer=42", + { "Content-Type" : "application/json;charset=UTF-8" }, + this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_equals(page.plainText, "request handled"); + })); + +}, "handling POST with ill-formed application/json data"); + +async_test(function () { + var page = require("webpage").create(); + var url = "http://localhost:"+port+"/"; + var fs = require("fs"); + var png = fs.read(fs.join(phantom.libraryPath, + "../../www/phantomjs.png"), "b"); + + arm_check_request(this, false, png, "image/png"); + page.open(url, "get", this.step_func_done(function (status) { + assert_equals(status, "success"); + function checkImg() { + var img = document.querySelector("img"); + if (img) { + return { w: img.width, h: img.height }; + } else { + return {}; + } + } + // XFAIL: image doesn't load properly and we receive the dimensions of + // the ?-in-a-box placeholder + assert_deep_equals(page.evaluate(checkImg), { w: 200, h: 200 }); + })); + +}, "handling binary data", { + skip: true, // crash: https://github.com/ariya/phantomjs/issues/13461 + expected_fail: true // received image is corrupt: + // https://github.com/ariya/phantomjs/issues/13026 + // and perhaps others +}); diff --git a/test/node_modules/dummy_exposed.js b/test/node_modules/dummy_exposed.js new file mode 100644 index 0000000..143519b --- /dev/null +++ b/test/node_modules/dummy_exposed.js @@ -0,0 +1 @@ +module.exports = module; diff --git a/test/node_modules/dummy_file.js b/test/node_modules/dummy_file.js new file mode 100644 index 0000000..a18d8a9 --- /dev/null +++ b/test/node_modules/dummy_file.js @@ -0,0 +1 @@ +module.exports = 'spec/node_modules/dummy_file'; diff --git a/test/node_modules/dummy_file2.js b/test/node_modules/dummy_file2.js new file mode 100644 index 0000000..77e02ec --- /dev/null +++ b/test/node_modules/dummy_file2.js @@ -0,0 +1 @@ +module.exports = 'spec/node_modules/dummy_file2'; diff --git a/test/regression/README b/test/regression/README new file mode 100644 index 0000000..2e8394c --- /dev/null +++ b/test/regression/README @@ -0,0 +1,3 @@ +Tests in this directory are named for their bug number. +pjs-NNNN corresponds to https://github.com/ariya/phantomjs/issues/NNNN. +webkit-NNNN corresponds to ​https://bugs.webkit.org/show_bug.cgi?id=NNNN. diff --git a/test/regression/pjs-10690.js b/test/regression/pjs-10690.js new file mode 100644 index 0000000..7e45c82 --- /dev/null +++ b/test/regression/pjs-10690.js @@ -0,0 +1,14 @@ +// Issue 10690: the second page load used to crash on OSX. + +var url = TEST_HTTP_BASE + 'regression/pjs-10690/index.html'; +function do_test() { + var page = require('webpage').create(); + + page.open(url, this.step_func_done (function (status) { + assert_equals(status, "success"); + page.release(); + })); +} + +async_test(do_test, "load a page with a downloadable font, once"); +async_test(do_test, "load a page with a downloadable font, again"); diff --git a/test/regression/pjs-12482.js b/test/regression/pjs-12482.js new file mode 100644 index 0000000..7df89dd --- /dev/null +++ b/test/regression/pjs-12482.js @@ -0,0 +1,48 @@ +//! no-harness + +// https://github.com/ariya/phantomjs/issues/12482 +// regression caused by fix for +// https://github.com/ariya/phantomjs/issues/12431 + +var webpage = require('webpage'); +var sys = require('system'); + +var pages = [ + webpage.create(), + webpage.create(), + webpage.create() +]; + +var loaded = 0; + +sys.stdout.write("1.." + pages.length + "\n"); +setTimeout(function () { phantom.exit(1); }, 200); + +function loadHook (status) { + loaded++; + if (status === "success") { + sys.stdout.write("ok " + loaded + " loading page\n"); + } else { + sys.stdout.write("not ok " + loaded + " loading page\n"); + } + + if (loaded === pages.length) { + pages[1].close(); + setTimeout(function(){ + phantom.exit(0); + sys.stdout.write("not ok " + (pages.length+1) + + " should not get here # TODO\n"); + }, 50); + } +} +function consoleHook (msg) { + sys.stdout.write(msg + "\n"); +} + +for (var i = 0; i < pages.length; i++) { + pages[i].onConsoleMessage = consoleHook; + pages[i].open( + "data:text/html,", + loadHook); +} diff --git a/test/regression/pjs-13551.js b/test/regression/pjs-13551.js new file mode 100644 index 0000000..844e219 --- /dev/null +++ b/test/regression/pjs-13551.js @@ -0,0 +1,51 @@ +// Issue #13551: Crash when switching "back" from frame that no longer +// exists (for whatever reason) + +var webpage = require('webpage'); + +function test_template(parent, action) { + var page; + var url = TEST_HTTP_BASE + + "/regression/pjs-13551/" + parent + "-parent.html"; + var s_callback0, s_callback1, s_callback2; + + function callback0 (n) { + assert_equals(n, 0); + page.onCallback = s_callback1; + page.evaluate(function () { + document.getElementById("prepare").click(); + }); + } + function callback1 (n) { + assert_equals(n, 1); + page.onCallback = s_callback2; + assert_equals(page.switchToFrame("target"), true); + assert_equals(page.switchToFrame("actor"), true); + page.evaluate(function () { + document.getElementById("execute").click(); + }); + } + function callback2 (n) { + assert_equals(n, 2); + assert_is_true(action == 'main' || action == 'parent'); + if (action == 'main') { + page.switchToMainFrame(); // Crash here + } else { + page.switchToParentFrame(); // Or here + } + } + + return function test_action () { + page = webpage.create(); + s_callback0 = this.step_func(callback0); + s_callback1 = this.step_func(callback1); + s_callback2 = this.step_func_done(callback2); + page.onCallback = s_callback0; + page.open(url); + }; +} + +async_test(test_template('closing', 'main'), "main from closed"); +async_test(test_template('closing', 'parent'), "parent from closed"); +async_test(test_template('reloading', 'main'), "main from reloaded"); +async_test(test_template('reloading', 'parent'), "parent from reloaded"); diff --git a/test/regression/webkit-60448.js b/test/regression/webkit-60448.js new file mode 100644 index 0000000..4628093 --- /dev/null +++ b/test/regression/webkit-60448.js @@ -0,0 +1,12 @@ +var url = TEST_HTTP_BASE + "regression/webkit-60448.html"; + +async_test(function () { + var p = require("webpage").create(); + p.open(url, this.step_func_done(function (status) { + assert_equals(status, "success"); + assert_is_true(p.evaluate(function () { + return document.getElementById("test") === null; + })); + })); +}, +"remove an inline HTML element from the document"); diff --git a/test/run-tests-ghostdriver.sh b/test/run-tests-ghostdriver.sh new file mode 100755 index 0000000..8b4a937 --- /dev/null +++ b/test/run-tests-ghostdriver.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Go to GhostDriver (Java) Tests +pushd ./test/ghostdriver-test/java + +# Ensure Gradle Wrapper is executable +chmod +x ./gradlew + +# Run tests +./gradlew test -q +# Grab exit status +TEST_EXIT_STATUS=$? + +# Return to starting directory +popd + +exit $TEST_EXIT_STATUS diff --git a/test/run-tests.py b/test/run-tests.py new file mode 100755 index 0000000..060d53f --- /dev/null +++ b/test/run-tests.py @@ -0,0 +1,1034 @@ +#!/usr/bin/env python + +import argparse +import collections +import errno +import glob +import imp +import os +import platform +import posixpath +import re +import shlex +import SimpleHTTPServer +import socket +import SocketServer +import ssl +import string +import cStringIO as StringIO +import subprocess +import sys +import threading +import time +import traceback +import urllib + +# All files matching one of these glob patterns will be run as tests. +TESTS = [ + 'basics/*.js', + 'module/*/*.js', + 'standards/*/*.js', + 'regression/*.js', +] + +TIMEOUT = 7 # Maximum duration of PhantomJS execution (in seconds). + # This is a backstop; testharness.js imposes a shorter + # timeout. Both can be increased if necessary. + +# +# Utilities +# + +# FIXME: assumes ANSI/VT100 escape sequences +# properly this should use curses, but that's an awful lot of work +# One of colors 30 ("black" -- usually a dark gray) and 37 ("white" -- +# usually a very light gray) will almost certainly be illegible +# against the terminal background, so we provide neither. +# The colorization mode is global because so is sys.stdout. +_COLOR_NONE = { + "_": "", "^": "", + "r": "", "R": "", + "g": "", "G": "", + "y": "", "Y": "", + "b": "", "B": "", + "m": "", "M": "", + "c": "", "C": "", +} +_COLOR_ON = { + "_": "\033[0m", "^": "\033[1m", + "r": "\033[31m", "R": "\033[1;31m", + "g": "\033[32m", "G": "\033[1;32m", + "y": "\033[33m", "Y": "\033[1;33m", + "b": "\033[34m", "B": "\033[1;34m", + "m": "\033[35m", "M": "\033[1;35m", + "c": "\033[36m", "C": "\033[1;36m", +} +_COLOR_BOLD = { + "_": "\033[0m", "^": "\033[1m", + "r": "\033[0m", "R": "\033[1m", + "g": "\033[0m", "G": "\033[1m", + "y": "\033[0m", "Y": "\033[1m", + "b": "\033[0m", "B": "\033[1m", + "m": "\033[0m", "M": "\033[1m", + "c": "\033[0m", "C": "\033[1m", +} +_COLORS = None +def activate_colorization(options): + global _COLORS + if options.color == "always": + _COLORS = _COLOR_ON + elif options.color == "never": + _COLORS = _COLOR_NONE + else: + if sys.stdout.isatty() and platform.system() != "Windows": + try: + n = int(subprocess.check_output(["tput", "colors"])) + if n >= 8: + _COLORS = _COLOR_ON + else: + _COLORS = _COLOR_BOLD + except subprocess.CalledProcessError: + _COLORS = _COLOR_NONE + else: + _COLORS = _COLOR_NONE + +def colorize(color, message): + return _COLORS[color] + message + _COLORS["_"] + +# create_default_context and SSLContext were only added in 2.7.9, +# which is newer than the python2 that ships with OSX :-( +# The fallback tries to mimic what create_default_context(CLIENT_AUTH) +# does. Security obviously isn't important in itself for a test +# server, but making sure the PJS client can talk to a server +# configured according to modern TLS best practices _is_ important. +# Unfortunately, there is no way to set things like OP_NO_SSL2 or +# OP_CIPHER_SERVER_PREFERENCE prior to 2.7.9. +CIPHERLIST_2_7_9 = ( + 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:' + 'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:' + '!eNULL:!MD5:!DSS:!RC4' +) +def wrap_socket_ssl(sock, base_path): + crtfile = os.path.join(base_path, 'certs/https-snakeoil.crt') + keyfile = os.path.join(base_path, 'certs/https-snakeoil.key') + + try: + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain(crtfile, keyfile) + return ctx.wrap_socket(sock, server_side=True) + + except AttributeError: + return ssl.wrap_socket(sock, + keyfile=keyfile, + certfile=crtfile, + server_side=True, + ciphers=CIPHERLIST_2_7_9) + +# This should be in the standard library somewhere, but as far as I +# can tell, it isn't. +class ResponseHookImporter(object): + def __init__(self, www_path): + # All Python response hooks, no matter how deep below www_path, + # are treated as direct children of the fake "test_www" package. + if 'test_www' not in sys.modules: + imp.load_source('test_www', www_path + '/__init__.py') + + self.tr = string.maketrans('-./%', '____') + + def __call__(self, path): + modname = 'test_www.' + path.translate(self.tr) + try: + return sys.modules[modname] + except KeyError: + return imp.load_source(modname, path) + +# This should also be in the standard library somewhere, and +# definitely isn't. +# +# FIXME: This currently involves *three* threads for every process, +# and a fourth if the process takes input. (On Unix, clever use of +# select() might be able to get that down to one, but zero is Hard. +# On Windows, we're hosed. 3.4's asyncio module would make everything +# better, but 3.4 is its own can of worms.) +try: + devnull = subprocess.DEVNULL +except: + devnull = os.open(os.devnull, os.O_RDONLY) + +def do_call_subprocess(command, verbose, stdin_data, timeout): + + def read_thread(linebuf, fp): + while True: + line = fp.readline().rstrip() + if not line: break # EOF + line = line.rstrip() + if line: + linebuf.append(line) + if verbose >= 3: + sys.stdout.write(line + '\n') + + def write_thread(data, fp): + fp.writelines(data) + fp.close() + + def reap_thread(proc, timed_out): + if proc.returncode is None: + proc.terminate() + timed_out[0] = True + + class DummyThread: + def start(self): pass + def join(self): pass + + if stdin_data: + stdin = subprocess.PIPE + else: + stdin = devnull + + proc = subprocess.Popen(command, + stdin=stdin, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + if stdin_data: + sithrd = threading.Thread(target=write_thread, + args=(stdin_data, proc.stdin)) + else: + sithrd = DummyThread() + + stdout = [] + stderr = [] + timed_out = [False] + sothrd = threading.Thread(target=read_thread, args=(stdout, proc.stdout)) + sethrd = threading.Thread(target=read_thread, args=(stderr, proc.stderr)) + rpthrd = threading.Timer(timeout, reap_thread, args=(proc, timed_out)) + + sithrd.start() + sothrd.start() + sethrd.start() + rpthrd.start() + + proc.wait() + if not timed_out[0]: rpthrd.cancel() + + sithrd.join() + sothrd.join() + sethrd.join() + rpthrd.join() + + if timed_out[0]: + stderr.append("TIMEOUT: Process terminated after {} seconds." + .format(timeout)) + if verbose >= 3: + sys.stdout.write(stderr[-1] + "\n") + + rc = proc.returncode + if verbose >= 3: + if rc < 0: + sys.stdout.write("## killed by signal {}\n".format(-rc)) + else: + sys.stdout.write("## exit {}\n".format(rc)) + return proc.returncode, stdout, stderr + +# +# HTTP/HTTPS server, presented on localhost to the tests +# + +class FileHandler(SimpleHTTPServer.SimpleHTTPRequestHandler, object): + + def __init__(self, *args, **kwargs): + self._cached_untranslated_path = None + self._cached_translated_path = None + self.postdata = None + super(FileHandler, self).__init__(*args, **kwargs) + + # silent, do not pollute stdout nor stderr. + def log_message(self, format, *args): + return + + # accept POSTs, read the postdata and stash it in an instance variable, + # then forward to do_GET; handle_request hooks can vary their behavior + # based on the presence of postdata and/or the command verb. + def do_POST(self): + try: + ln = int(self.headers.get('content-length')) + except TypeError, ValueError: + self.send_response(400, 'Bad Request') + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write("No or invalid Content-Length in POST (%r)" + % self.headers.get('content-length')) + return + + self.postdata = self.rfile.read(ln) + self.do_GET() + + # allow provision of a .py file that will be interpreted to + # produce the response. + def send_head(self): + path = self.translate_path(self.path) + + # do not allow direct references to .py(c) files, + # or indirect references to __init__.py + if (path.endswith('.py') or path.endswith('.pyc') or + path.endswith('__init__')): + self.send_error(404, 'File not found') + return None + + if os.path.exists(path): + return super(FileHandler, self).send_head() + + py = path + '.py' + if os.path.exists(py): + try: + mod = self.get_response_hook(py) + return mod.handle_request(self) + except: + self.send_error(500, 'Internal Server Error in '+py) + raise + + self.send_error(404, 'File not found') + return None + + # modified version of SimpleHTTPRequestHandler's translate_path + # to resolve the URL relative to the www/ directory + # (e.g. /foo -> test/www/foo) + def translate_path(self, path): + + # Cache for efficiency, since our send_head calls this and + # then, in the normal case, the parent class's send_head + # immediately calls it again. + if (self._cached_translated_path is not None and + self._cached_untranslated_path == path): + return self._cached_translated_path + + orig_path = path + + # Strip query string and/or fragment, if present. + x = path.find('?') + if x != -1: path = path[:x] + x = path.find('#') + if x != -1: path = path[:x] + + # Ensure consistent encoding of special characters, then + # lowercase everything so that the tests behave consistently + # whether or not the local filesystem is case-sensitive. + path = urllib.quote(urllib.unquote(path)).lower() + + # Prevent access to files outside www/. + # At this point we want specifically POSIX-like treatment of 'path' + # because it is still a URL component and not a filesystem path. + # SimpleHTTPRequestHandler.send_head() expects us to preserve the + # distinction between paths with and without a trailing slash, but + # posixpath.normpath() discards that distinction. + trailing_slash = path.endswith('/') + path = posixpath.normpath(path) + while path.startswith('/'): + path = path[1:] + while path.startswith('../'): + path = path[3:] + + # Now resolve the normalized, clamped path relative to the www/ + # directory, according to local OS conventions. + path = os.path.normpath(os.path.join(self.www_path, *path.split('/'))) + if trailing_slash: + # it must be a '/' even on Windows + path += '/' + + self._cached_untranslated_path = orig_path + self._cached_translated_path = path + return path + +class TCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + # This is how you are officially supposed to set SO_REUSEADDR per + # https://docs.python.org/2/library/socketserver.html#SocketServer.BaseServer.allow_reuse_address + allow_reuse_address = True + + def __init__(self, use_ssl, handler, base_path, signal_error): + SocketServer.TCPServer.__init__(self, ('localhost', 0), handler) + if use_ssl: + self.socket = wrap_socket_ssl(self.socket, base_path) + self._signal_error = signal_error + + def handle_error(self, request, client_address): + # Ignore errors which can occur naturally if the client + # disconnects in the middle of a request. EPIPE and + # ECONNRESET *should* be the only such error codes + # (according to the OSX manpage for send()). + _, exval, _ = sys.exc_info() + if getattr(exval, 'errno', None) in (errno.EPIPE, errno.ECONNRESET): + return + + # Otherwise, report the error to the test runner. + self._signal_error(sys.exc_info()) + +class HTTPTestServer(object): + def __init__(self, base_path, signal_error, verbose): + self.httpd = None + self.httpsd = None + self.base_path = base_path + self.www_path = os.path.join(base_path, 'www') + self.signal_error = signal_error + self.verbose = verbose + + def __enter__(self): + handler = FileHandler + handler.extensions_map.update({ + '.htm': 'text/html', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json' + }) + handler.www_path = self.www_path + handler.get_response_hook = ResponseHookImporter(self.www_path) + + self.httpd = TCPServer(False, handler, + self.base_path, self.signal_error) + os.environ['TEST_HTTP_BASE'] = \ + 'http://localhost:{}/'.format(self.httpd.server_address[1]) + httpd_thread = threading.Thread(target=self.httpd.serve_forever) + httpd_thread.daemon = True + httpd_thread.start() + if self.verbose >= 3: + sys.stdout.write("## HTTP server at {}\n".format( + os.environ['TEST_HTTP_BASE'])) + + self.httpsd = TCPServer(True, handler, + self.base_path, self.signal_error) + os.environ['TEST_HTTPS_BASE'] = \ + 'https://localhost:{}/'.format(self.httpsd.server_address[1]) + httpsd_thread = threading.Thread(target=self.httpsd.serve_forever) + httpsd_thread.daemon = True + httpsd_thread.start() + if self.verbose >= 3: + sys.stdout.write("## HTTPS server at {}\n".format( + os.environ['TEST_HTTPS_BASE'])) + + return self + + def __exit__(self, *dontcare): + self.httpd.shutdown() + del os.environ['TEST_HTTP_BASE'] + self.httpsd.shutdown() + del os.environ['TEST_HTTPS_BASE'] + +# +# Running tests and interpreting their results +# +class TestDetailCode(collections.namedtuple("TestDetailCode", ( + "idx", "color", "short_label", "label", "long_label"))): + def __index__(self): return self.idx + def __hash__(self): return self.idx + def __eq__(self, other): return self.idx == other.idx + def __ne__(self, other): return self.idx != other.idx + +class T(object): + PASS = TestDetailCode(0, "g", ".", "pass", "passed") + FAIL = TestDetailCode(1, "R", "F", "FAIL", "failed") + XFAIL = TestDetailCode(2, "y", "f", "xfail", "failed as expected") + XPASS = TestDetailCode(3, "Y", "P", "XPASS", "passed unexpectedly") + ERROR = TestDetailCode(4, "R", "E", "ERROR", "had errors") + SKIP = TestDetailCode(5, "m", "s", "skip", "skipped") + MAX = 6 + +class TestDetail(object): + """Holds one block of details about a test that failed.""" + # types of details: + + def __init__(self, message, test_id, detail_type): + if not isinstance(message, list): + message = [message] + self.message = [line.rstrip() + for chunk in message + for line in chunk.split("\n")] + + self.dtype = detail_type + self.test_id = test_id + + def report(self, fp): + col, label = self.dtype.color, self.dtype.label + if self.test_id: + fp.write("{:>5}: {}\n".format(colorize(col, label), + self.test_id)) + lo = 0 + else: + fp.write("{:>5}: {}\n".format(colorize(col, label), + self.message[0])) + lo = 1 + for line in self.message[lo:]: + fp.write(" {}\n".format(colorize("b", line))) + +class TestGroup(object): + """Holds the result of one group of tests (that is, one .js file), + parsed from the output of run_phantomjs (see below). + Subclasses specify what the output means. + A test with zero details is considered to be successful. + """ + + def __init__(self, name): + self.name = name + self.n = [0]*T.MAX + self.details = [] + + def parse(self, rc, out, err): + raise NotImplementedError + + def _add_d(self, message, test_id, dtype): + self.n[dtype] += 1 + self.details.append(TestDetail(message, test_id, dtype)) + + def add_pass (self, m, t): self._add_d(m, t, T.PASS) + def add_fail (self, m, t): self._add_d(m, t, T.FAIL) + def add_xpass(self, m, t): self._add_d(m, t, T.XPASS) + def add_xfail(self, m, t): self._add_d(m, t, T.XFAIL) + def add_error(self, m, t): self._add_d(m, t, T.ERROR) + def add_skip (self, m, t): self._add_d(m, t, T.SKIP) + + def default_interpret_exit_code(self, rc): + if rc == 0: + if not self.is_successful() and not self.n[T.ERROR]: + self.add_error([], + "PhantomJS exited successfully when test failed") + + # Exit code -15 indicates a timeout. + elif rc == 1 or rc == -15: + if self.is_successful(): + self.add_error([], "PhantomJS exited unsuccessfully") + + elif rc >= 2: + self.add_error([], "PhantomJS exited with code {}".format(rc)) + else: + self.add_error([], "PhantomJS killed by signal {}".format(-rc)) + + def is_successful(self): + return self.n[T.FAIL] + self.n[T.XPASS] + self.n[T.ERROR] == 0 + + def worst_code(self): + # worst-to-best ordering + for code in (T.ERROR, T.FAIL, T.XPASS, T.SKIP, T.XFAIL, T.PASS): + if self.n[code] > 0: + return code + return T.PASS + + def one_char_summary(self, fp): + code = self.worst_code() + fp.write(colorize(code.color, code.short_label)) + fp.flush() + + def line_summary(self, fp): + code = self.worst_code() + fp.write("{}: {}\n".format(colorize("^", self.name), + colorize(code.color, code.label))) + + def report(self, fp, show_all): + self.line_summary(fp) + need_blank_line = False + for detail in self.details: + if show_all or detail.dtype not in (T.PASS, T.XFAIL, T.SKIP): + detail.report(fp) + need_blank_line = True + if need_blank_line: + fp.write("\n") + + def report_for_verbose_level(self, fp, verbose): + if verbose == 0: + self.one_char_summary(sys.stdout) + elif verbose == 1: + self.report(sys.stdout, False) + else: + self.report(sys.stdout, True) + +class ExpectTestGroup(TestGroup): + """Test group whose output must be exactly as specified by directives + in the file. This is how you test for an _unsuccessful_ exit code, + or for output appearing on a specific one of stdout/stderr. + """ + def __init__(self, name, rc_exp, stdout_exp, stderr_exp, + rc_xfail, stdout_xfail, stderr_xfail): + TestGroup.__init__(self, name) + if rc_exp is None: rc_exp = 0 + self.rc_exp = rc_exp + self.stdout_exp = stdout_exp + self.stderr_exp = stderr_exp + self.rc_xfail = rc_xfail + self.stdout_xfail = stdout_xfail + self.stderr_xfail = stderr_xfail + + def parse(self, rc, out, err): + self.parse_output("stdout", self.stdout_exp, out, self.stdout_xfail) + self.parse_output("stderr", self.stderr_exp, err, self.stderr_xfail) + + exit_msg = ["expected exit code {} got {}" + .format(self.rc_exp, rc)] + + if rc != self.rc_exp: + exit_desc = "did not exit as expected" + if self.rc_xfail: + self.add_xfail(exit_msg, exit_desc) + else: + self.add_fail(exit_msg, exit_desc) + else: + exit_desc = "exited as expected" + if self.rc_xfail: + self.add_xpass(exit_msg, exit_desc) + else: + self.add_pass(exit_msg, exit_desc) + + def parse_output(self, what, exp, got, xfail): + diff = [] + le = len(exp) + lg = len(got) + for i in range(max(le, lg)): + e = "" + g = "" + if i < le: e = exp[i] + if i < lg: g = got[i] + if e != g: + diff.extend(("{}: line {} not as expected".format(what, i+1), + "-" + repr(e)[1:-1], + "+" + repr(g)[1:-1])) + + if diff: + desc = what + " not as expected" + if xfail: + self.add_xfail(diff, desc) + else: + self.add_fail(diff, desc) + else: + desc = what + " as expected" + if xfail: + self.add_xpass(diff, desc) + else: + self.add_pass(diff, desc) + + +class TAPTestGroup(TestGroup): + """Test group whose output is interpreted according to a variant of the + Test Anything Protocol (http://testanything.org/tap-specification.html). + + Relative to that specification, these are the changes: + + * Plan-at-the-end, explanations for directives, and "Bail out!" + are not supported. ("1..0 # SKIP: explanation" *is* supported.) + * "Anything else" lines are an error. + * Repeating a test point number, or using one outside the plan + range, is an error (this is unspecified in TAP proper). + * Diagnostic lines beginning with # are taken as additional + information about the *next* test point. Diagnostic lines + beginning with ## are ignored. + * Directives are case sensitive. + + """ + + diag_r = re.compile(r"^#(#*)\s*(.*)$") + plan_r = re.compile(r"^1..(\d+)(?:\s*\#\s*SKIP(?::\s*(.*)))?$") + test_r = re.compile(r"^(not ok|ok)\s*" + r"([0-9]+)?\s*" + r"([^#]*)(?:# (TODO|SKIP))?$") + + def parse(self, rc, out, err): + self.parse_tap(out, err) + self.default_interpret_exit_code(rc) + + def parse_tap(self, out, err): + points_already_used = set() + messages = [] + + # Look for the plan. + # Diagnostic lines are allowed to appear above the plan, but not + # test lines. + for i in range(len(out)): + line = out[i] + m = self.diag_r.match(line) + if m: + if not m.group(1): + messages.append(m.group(2)) + continue + + m = self.plan_r.match(line) + if m: + break + + messages.insert(0, line) + self.add_error(messages, "Plan line not interpretable") + if i + 1 < len(out): + self.add_skip(out[(i+1):], "All further output ignored") + return + else: + self.add_error(messages, "No plan line detected in output") + return + + max_point = int(m.group(1)) + if max_point == 0: + if any(msg.startswith("ERROR:") for msg in messages): + self.add_error(messages, m.group(2) or "Test group skipped") + else: + self.add_skip(messages, m.group(2) or "Test group skipped") + if i + 1 < len(out): + self.add_skip(out[(i+1):], "All further output ignored") + return + + prev_point = 0 + + for i in range(i+1, len(out)): + line = out[i] + m = self.diag_r.match(line) + if m: + if not m.group(1): + messages.append(m.group(2)) + continue + m = self.test_r.match(line) + if m: + status = m.group(1) + point = m.group(2) + desc = m.group(3) + dirv = m.group(4) + + if point: + point = int(point) + else: + point = prev_point + 1 + + if point in points_already_used: + # A reused test point is an error. + self.add_error(messages, desc + " [test point repeated]") + else: + points_already_used.add(point) + # A point above the plan limit is an automatic *fail*. + # The test suite relies on this in testing exit(). + if point > max_point: + status = "not ok" + + if status == "ok": + if not dirv: + self.add_pass(messages, desc) + elif dirv == "TODO": + self.add_xpass(messages, desc) + elif dirv == "SKIP": + self.add_skip(messages, desc) + else: + self.add_error(messages, desc + + " [ok, with invalid directive "+dirv+"]") + else: + if not dirv: + self.add_fail(messages, desc) + elif dirv == "TODO": + self.add_xfail(messages, desc) + else: + self.add_error(messages, desc + + " [not ok, with invalid directive "+dirv+"]") + + del messages[:] + prev_point = point + + else: + self.add_error([line], "neither a test nor a diagnostic") + + # Any output on stderr is an error, with one exception: the timeout + # message added by record_process_output, which is treated as an + # unnumbered "not ok". + if err: + if len(err) == 1 and err[0].startswith("TIMEOUT: "): + points_already_used.add(prev_point + 1) + self.add_fail(messages, err[0][len("TIMEOUT: "):]) + else: + self.add_error(err, "Unexpected output on stderr") + + # Any missing test points are fails. + for pt in range(1, max_point+1): + if pt not in points_already_used: + self.add_fail([], "test {} did not report status".format(pt)) + +class TestRunner(object): + def __init__(self, base_path, phantomjs_exe, options): + self.base_path = base_path + self.cert_path = os.path.join(base_path, 'certs') + self.harness = os.path.join(base_path, 'testharness.js') + self.phantomjs_exe = phantomjs_exe + self.verbose = options.verbose + self.debugger = options.debugger + self.to_run = options.to_run + self.server_errs = [] + + def signal_server_error(self, exc_info): + self.server_errs.append(exc_info) + + def get_base_command(self, debugger): + if debugger is None: + return [self.phantomjs_exe] + elif debugger == "gdb": + return ["gdb", "--args", self.phantomjs_exe] + elif debugger == "lldb": + return ["lldb", "--", self.phantomjs_exe] + elif debugger == "valgrind": + return ["valgrind", self.phantomjs_exe] + else: + raise RuntimeError("Don't know how to invoke " + self.debugger) + + def run_phantomjs(self, script, + script_args=[], pjs_args=[], stdin_data=[], + timeout=TIMEOUT, silent=False): + verbose = self.verbose + debugger = self.debugger + if silent: + verbose = False + debugger = None + + output = [] + command = self.get_base_command(debugger) + command.extend(pjs_args) + command.append(script) + if verbose: + command.append('--verbose={}'.format(verbose)) + command.extend(script_args) + + if verbose >= 3: + sys.stdout.write("## running {}\n".format(" ".join(command))) + + if debugger: + # FIXME: input-feed mode doesn't work with a debugger, + # because how do you tell the debugger that the *debuggee* + # needs to read from a pipe? + subprocess.call(command) + return 0, [], [] + else: + return do_call_subprocess(command, verbose, stdin_data, timeout) + + def run_test(self, script, name): + script_args = [] + pjs_args = [] + use_harness = True + use_snakeoil = True + stdin_data = [] + stdout_exp = [] + stderr_exp = [] + rc_exp = None + stdout_xfail = False + stderr_xfail = False + rc_xfail = False + timeout = TIMEOUT + + def require_args(what, i, tokens): + if i+1 == len(tokens): + raise ValueError(what + "directive requires an argument") + + if self.verbose >= 3: + sys.stdout.write(colorize("^", name) + ":\n") + # Parse any directives at the top of the script. + try: + with open(script, "rt") as s: + for line in s: + if not line.startswith("//!"): + break + tokens = shlex.split(line[3:], comments=True) + + skip = False + for i in range(len(tokens)): + if skip: + skip = False + continue + tok = tokens[i] + if tok == "no-harness": + use_harness = False + elif tok == "no-snakeoil": + use_snakeoil = False + elif tok == "expect-exit-fails": + rc_xfail = True + elif tok == "expect-stdout-fails": + stdout_xfail = True + elif tok == "expect-stderr-fails": + stderr_xfail = True + elif tok == "timeout:": + require_args(tok, i, tokens) + timeout = float(tokens[i+1]) + if timeout <= 0: + raise ValueError("timeout must be positive") + skip = True + elif tok == "expect-exit:": + require_args(tok, i, tokens) + rc_exp = int(tokens[i+1]) + skip = True + elif tok == "phantomjs:": + require_args(tok, i, tokens) + pjs_args.extend(tokens[(i+1):]) + break + elif tok == "script:": + require_args(tok, i, tokens) + script_args.extend(tokens[(i+1):]) + break + elif tok == "stdin:": + require_args(tok, i, tokens) + stdin_data.append(" ".join(tokens[(i+1):]) + "\n") + break + elif tok == "expect-stdout:": + require_args(tok, i, tokens) + stdout_exp.append(" ".join(tokens[(i+1):])) + break + elif tok == "expect-stderr:": + require_args(tok, i, tokens) + stderr_exp.append(" ".join(tokens[(i+1):])) + break + else: + raise ValueError("unrecognized directive: " + tok) + + except Exception as e: + grp = TestGroup(name) + if hasattr(e, 'strerror') and hasattr(e, 'filename'): + grp.add_error([], '{} ({}): {}\n' + .format(name, e.filename, e.strerror)) + else: + grp.add_error([], '{} ({}): {}\n' + .format(name, script, str(e))) + return grp + + if use_harness: + script_args.insert(0, script) + script = self.harness + + if use_snakeoil: + pjs_args.insert(0, '--ssl-certificates-path=' + self.cert_path) + + rc, out, err = self.run_phantomjs(script, script_args, pjs_args, + stdin_data, timeout) + + if rc_exp or stdout_exp or stderr_exp: + grp = ExpectTestGroup(name, + rc_exp, stdout_exp, stderr_exp, + rc_xfail, stdout_xfail, stderr_xfail) + else: + grp = TAPTestGroup(name) + grp.parse(rc, out, err) + return grp + + def run_tests(self): + start = time.time() + base = self.base_path + nlen = len(base) + 1 + + results = [] + + for test_glob in TESTS: + test_glob = os.path.join(base, test_glob) + + for test_script in sorted(glob.glob(test_glob)): + tname = os.path.splitext(test_script)[0][nlen:] + if self.to_run: + for to_run in self.to_run: + if to_run in tname: + break + else: + continue + + any_executed = True + grp = self.run_test(test_script, tname) + grp.report_for_verbose_level(sys.stdout, self.verbose) + results.append(grp) + + grp = TestGroup("HTTP server errors") + for ty, val, tb in self.server_errs: + grp.add_error(traceback.format_tb(tb, 5), + traceback.format_exception_only(ty, val)[-1]) + grp.report_for_verbose_level(sys.stdout, self.verbose) + results.append(grp) + + sys.stdout.write("\n") + return self.report(results, time.time() - start) + + def report(self, results, elapsed): + # There is always one test group, for the HTTP server errors. + if len(results) == 1: + sys.stderr.write("No tests selected for execution.\n") + return 1 + + n = [0] * T.MAX + + for grp in results: + if self.verbose == 0 and not grp.is_successful(): + grp.report(sys.stdout, False) + for i, x in enumerate(grp.n): n[i] += x + + sys.stdout.write("{:6.3f}s elapsed\n".format(elapsed)) + for s in (T.PASS, T.FAIL, T.XPASS, T.XFAIL, T.ERROR, T.SKIP): + if n[s]: + sys.stdout.write(" {:>4} {}\n".format(n[s], s.long_label)) + + if n[T.FAIL] == 0 and n[T.XPASS] == 0 and n[T.ERROR] == 0: + return 0 + else: + return 1 + +def init(): + base_path = os.path.normpath(os.path.dirname(os.path.abspath(__file__))) + phantomjs_exe = os.path.normpath(base_path + '/../bin/phantomjs') + if sys.platform in ('win32', 'cygwin'): + phantomjs_exe += '.exe' + if not os.path.isfile(phantomjs_exe): + sys.stdout.write("{} is unavailable, cannot run tests.\n" + .format(phantomjs_exe)) + sys.exit(1) + + parser = argparse.ArgumentParser(description='Run PhantomJS tests.') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase verbosity of logs (repeat for more)') + parser.add_argument('to_run', nargs='*', metavar='test', + help='tests to run (default: all of them)') + parser.add_argument('--debugger', default=None, + help="Run PhantomJS under DEBUGGER") + parser.add_argument('--color', metavar="WHEN", default='auto', + choices=['always', 'never', 'auto'], + help="colorize the output; can be 'always'," + " 'never', or 'auto' (the default)") + + options = parser.parse_args() + activate_colorization(options) + runner = TestRunner(base_path, phantomjs_exe, options) + if options.verbose: + rc, ver, err = runner.run_phantomjs('--version', silent=True) + if rc != 0 or len(ver) != 1 or len(err) != 0: + sys.stdout.write(colorize("R", "FATAL")+": Version check failed\n") + for l in ver: + sys.stdout.write(colorize("b", "## " + l) + "\n") + for l in err: + sys.stdout.write(colorize("b", "## " + l) + "\n") + sys.stdout.write(colorize("b", "## exit {}".format(rc)) + "\n") + sys.exit(1) + + sys.stdout.write(colorize("b", "## Testing PhantomJS "+ver[0])+"\n") + + # Run all the tests in Chatham Islands Standard Time, UTC+12:45. + # This timezone is deliberately chosen to be unusual: it's not a + # whole number of hours offset from UTC *and* it's more than twelve + # hours offset from UTC. + # + # The Chatham Islands do observe daylight savings, but we don't + # implement that because testsuite issues only reproducible on two + # particular days out of the year are too much tsuris. + # + # Note that the offset in a TZ value is the negative of the way it's + # usually written, e.g. UTC+1 would be xxx-1:00. + os.environ["TZ"] = "CIST-12:45:00" + + return runner + +def main(): + runner = init() + try: + with HTTPTestServer(runner.base_path, + runner.signal_server_error, + runner.verbose): + sys.exit(runner.run_tests()) + + except Exception: + trace = traceback.format_exc(5).split("\n") + # there will be a blank line at the end of 'trace' + sys.stdout.write(colorize("R", "FATAL") + ": " + trace[-2] + "\n") + for line in trace[:-2]: + sys.stdout.write(colorize("b", "## " + line) + "\n") + + sys.exit(1) + + except KeyboardInterrupt: + sys.exit(2) + +main() diff --git a/test/standards/console/console_log.js b/test/standards/console/console_log.js new file mode 100644 index 0000000..7b17aba --- /dev/null +++ b/test/standards/console/console_log.js @@ -0,0 +1,9 @@ +async_test(function () { + var page = require('webpage').create(); + page.onConsoleMessage = this.step_func_done(function (msg) { + assert_equals(msg, "answer 42"); + }); + page.evaluate(function () { + console.log('answer', 42); + }); +}, "console.log should support multiple arguments"); diff --git a/test/standards/javascript/date.js b/test/standards/javascript/date.js new file mode 100644 index 0000000..b2a1f97 --- /dev/null +++ b/test/standards/javascript/date.js @@ -0,0 +1,12 @@ +test(function() { + var date = new Date('2012-09-07'); + assert_not_equals(date.toString(), 'Invalid Date'); + assert_equals(date.getUTCDate(), 7); + assert_equals(date.getUTCMonth(), 8); + assert_equals(date.getYear(), 112); +}, "new Date()"); + +test(function () { + var date = Date.parse("2012-01-01"); + assert_equals(date, 1325376000000); +}, "Date.parse()"); diff --git a/test/standards/javascript/function.js b/test/standards/javascript/function.js new file mode 100644 index 0000000..2b6f473 --- /dev/null +++ b/test/standards/javascript/function.js @@ -0,0 +1,46 @@ +test(function () { + assert_type_of(Function.length, 'number'); + assert_type_of(Function.prototype, 'function'); + assert_type_of(Function.prototype.apply, 'function'); + assert_type_of(Function.prototype.bind, 'function'); + assert_type_of(Function.prototype.call, 'function'); + assert_type_of(Function.prototype.name, 'string'); + assert_type_of(Function.prototype.toString, 'function'); +}, "Function properties"); + +test(function () { + var f = function foo(){}; + assert_equals(f.name, 'foo'); +}, ".name"); + +test(function () { + assert_equals(Function.length, 1); + assert_equals(function(){}.length, 0); + assert_equals(function(x){}.length, 1); + assert_equals(function(x, y){}.length, 2); + assert_equals(function(x, y){}.length, 2); +}, ".length"); + +test(function () { + var args, keys, str, enumerable; + (function() { + args = arguments; + keys = Object.keys(arguments); + str = JSON.stringify(arguments); + enumerable = false; + for (var i in arguments) enumerable = true; + })(14); + + assert_type_of(args, 'object'); + assert_type_of(args.length, 'number'); + assert_equals(args.toString(), '[object Arguments]'); + assert_equals(args.length, 1); + assert_equals(args[0], 14); + + assert_type_of(keys.length, 'number'); + assert_equals(keys.length, 1); + assert_equals(keys[0], "0"); + + assert_equals(str, '{"0":14}'); + assert_is_true(enumerable); +}, "arguments object"); diff --git a/test/testharness.js b/test/testharness.js new file mode 100644 index 0000000..f68cc6b --- /dev/null +++ b/test/testharness.js @@ -0,0 +1,1489 @@ +/* vim: set expandtab shiftwidth=4 tabstop=4: */ +/*global require, phantom, setTimeout, clearTimeout */ + +/* + This file is part of the PhantomJS project from Ofi Labs. + + Copyright 2015 Zachary Weinberg + + Based on testharness.js + produced by the W3C and distributed under the W3C 3-Clause BSD + License . + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +(function () { + +"use strict"; + +/** Public API: Defining and running tests. + * + * Compared to the W3C testharness.js, a number of minor changes have + * been made to fit the rather different PhantomJS controller + * environment, but the basic API is the same. + */ + +/** Define an asynchronous test. Returns a Test object. In response + to appropriate events, call the Test object's step() method, + passing a function containing assertions (as for synch tests). + Eventually, call the Test object's done() method (inside a step). + + All arguments are optional. If |func| is provided, it is called + immediately (after the previous test completes) as the first step + function. |properties| is the same as for test(). + + Test has several convenience methods for creating step functions, + see below. */ +function async_test(func, name, properties) { + var test_obj; + + if (func && typeof func !== "function") { + properties = name; + name = func; + func = null; + } + test_obj = new Test(test_name(func, name), properties); + if (func) { + test_obj.step(func); + } + return test_obj; +} +expose(async_test, 'async_test'); + +/** Define a synchronous test, which is just an asynchronous test that + calls done() immediately after its first step function (func) returns. + + |func| is a function containing the test code, and |name| + (optionally) is a descriptive name for the test. |func| will be + called (after the previous test completes), with |this| a Test + object (see below), and should make use of the global assert_* + functions. |properties| is an optional dictionary of test + properties; currently only one is defined: + + timeout - Timeout for this test, in milliseconds. */ +function test(func, name, properties) { + return async_test(function sync_step () { + func.call(this); + this.done(); + }, name, properties); +} +expose(test, 'test'); + +/** Define a series of synchronous tests all at once. + Easiest to explain by example: + + generate_tests(assert_equals, [ + ["Sum one and one", 1+1, 2], + ["Sum one and zero", 1+0, 1] + ]); + + is equivalent to + + test(function() {assert_equals(1+1, 2)}, "Sum one and one"); + test(function() {assert_equals(1+0, 1)}, "Sum one and zero"); + + The first argument can be an arbitrary function, and you can provide + as many arguments in each test vector entry as you like. + + The properties argument can be a single dictionary, which is applied to + all the tests, or an array, applied entry-by-entry. + */ +function generate_tests(func, args, properties) { + function generate_one_test(argv) { + return function generated_step () { + // 'this' will be set by bind() inside test(). + func.apply(this, argv); + }; + } + + var i; + for (i = 0; i < args.length; i++) { + test(generate_one_test(args[i].slice(1)), + args[i][0], + Array.isArray(properties) ? properties[i] : properties); + } +} +expose(generate_tests, 'generate_tests'); + +/** Set up the test harness. Does nothing if called after any test has + begun execution. May be called as setup(func), setup(properties), or + setup(func, properties). |func| is a function to call synchronously; + if it throws an exception the entire test group is considered failed. + |properties| is a dictionary containing one or more of these keys: + + explicit_done - Wait for an explicit call to done() before + declaring all tests complete (see below; implicitly true for + single-test files) + + allow_uncaught_exception - Don't treat an uncaught exception from + non-test code as an error. (Exceptions thrown out of test + functions are still errors.) + + timeout - Global timeout in milliseconds (default: 5 seconds) + test_timeout - Per-test timeout in milliseconds, unless overridden + by the timeout property on a specific test (default: none) + */ +function setup(func_or_properties, maybe_properties) { + var func = null, + properties = {}; + + if (arguments.length === 2) { + func = func_or_properties; + properties = maybe_properties; + } else if (typeof func_or_properties === "function") { + func = func_or_properties; + } else { + properties = func_or_properties; + } + tests.setup(func, properties); +} +expose(setup, 'setup'); + +/** Signal that all tests are complete. Must be called explicitly if + setup({explicit_done: true}) was used; otherwise implicitly + happens when all individual tests are done. */ +function done() { + tests.end_wait(); +} +expose(done, 'done'); + + +/** Public API: Assertions. + * All assertion functions take a |description| argument which is used to + * annotate any failing tests. + */ + +/** Assert that |actual| is strictly true. */ +function assert_is_true(actual, description) { + assert(actual === true, "assert_is_true", description, + "expected true got ${actual}", {actual: actual}); +} +expose(assert_is_true, 'assert_is_true'); + +/** Assert that |actual| is strictly false. */ +function assert_is_false(actual, description) { + assert(actual === false, "assert_is_false", description, + "expected false got ${actual}", {actual: actual}); +} +expose(assert_is_false, 'assert_is_false'); + +/** Assert that |actual| is strictly equal to |expected|. + The test is even more stringent than === (see same_value, below). */ +function assert_equals(actual, expected, description) { + if (typeof actual !== typeof expected) { + assert(false, "assert_equals", description, + "expected (${expectedT}) ${expected} " + + "but got (${actualT}) ${actual}", + {expectedT: typeof expected, + expected: expected, + actualT: typeof actual, + actual: actual}); + } + assert(same_value(actual, expected), "assert_equals", description, + "expected ${expected} but got ${actual}", + {expected: expected, actual: actual}); +} +expose(assert_equals, 'assert_equals'); + +/** Assert that |actual| is not strictly equal to |expected|, using the + same extra-stringent criterion as for assert_equals. */ +function assert_not_equals(actual, expected, description) { + if (typeof actual !== typeof expected) { + return; + } + assert(!same_value(actual, expected), "assert_not_equals", description, + "got disallowed value ${actual}", + {actual: actual}); +} +expose(assert_not_equals, 'assert_not_equals'); + +/** Assert that |expected|, a duck-typed array, contains |actual|, + according to indexOf. */ +function assert_in_array(actual, expected, description) { + assert(expected.indexOf(actual) !== -1, "assert_in_array", description, + "value ${actual} not in array ${expected}", + {actual: actual, expected: expected}); +} +expose(assert_in_array, 'assert_in_array'); + +/** Assert that |expected| and |actual| have all the same properties, + which are, recursively, strictly equal. For primitive types this + is the same as |assert_equals|. + */ +function assert_deep_equals(actual, expected, description) { + var stack = []; + function check_equal_r(act, exp) { + if (is_primitive_value(exp) || is_primitive_value(act)) { + assert(same_value(act, exp), + "assert_deep_equals", description, + "expected ${exp} but got ${act}" + + " (top level: expected ${expected} but got ${actual})", + {exp: exp, act: act, expected: expected, actual: actual}); + + } else if (stack.indexOf(act) === -1) { + var ka = {}, ke = {}, k; + stack.push(act); + + Object.getOwnPropertyNames(actual).forEach(function (x) { + ka[x] = true; + }); + Object.getOwnPropertyNames(expected).forEach(function (x) { + ke[x] = true; + }); + + for (k in ke) { + assert(k in ka, + "assert_deep_equals", description, + "expected property ${k} missing" + + " (top level: expected ${expected} but got ${actual})", + {k: k, expected: expected, actual: actual}); + + check_equal_r(act[k], exp[k]); + delete ka[k]; + } + for (k in ka) { + assert(false, "assert_deep_equals", description, + "unexpected property ${k}" + + " (top level: expected ${expected} but got ${actual})", + {k: k, expected: expected, actual: actual}); + } + + stack.pop(); + } + } + check_equal_r(actual, expected); +} +expose(assert_deep_equals, 'assert_deep_equals'); + +/** Assert that |expected| and |actual|, both primitive numbers, are + within |epsilon| of each other. */ +function assert_approx_equals(actual, expected, epsilon, description) { + assert(typeof actual === "number", + "assert_approx_equals", description, + "expected a number but got a ${type_actual}", + {type_actual: typeof actual}); + assert(typeof expected === "number", + "assert_approx_equals", description, + "expectation should be a number, got a ${type_expected}", + {type_expected: typeof expected}); + assert(typeof epsilon === "number", + "assert_approx_equals", description, + "epsilon should be a number but got a ${type_epsilon}", + {type_epsilon: typeof epsilon}); + + assert(Math.abs(actual - expected) <= epsilon, + "assert_approx_equals", description, + "expected ${expected} +/- ${epsilon} but got ${actual}", + {expected: expected, actual: actual, epsilon: epsilon}); +} +expose(assert_approx_equals, 'assert_approx_equals'); + +/** Assert that |actual| is less than |expected|, where both are + primitive numbers. */ +function assert_less_than(actual, expected, description) { + assert(typeof actual === "number", + "assert_less_than", description, + "expected a number but got a ${type_actual}", + {type_actual: typeof actual}); + assert(typeof expected === "number", + "assert_approx_equals", description, + "expectation should be a number, got a ${type_expected}", + {type_expected: typeof expected}); + + assert(actual < expected, + "assert_less_than", description, + "expected a number less than ${expected} but got ${actual}", + {expected: expected, actual: actual}); +} +expose(assert_less_than, 'assert_less_than'); + +/** Assert that |actual| is greater than |expected|, where both are + primitive numbers. */ +function assert_greater_than(actual, expected, description) { + assert(typeof actual === "number", + "assert_greater_than", description, + "expected a number but got a ${type_actual}", + {type_actual: typeof actual}); + assert(typeof expected === "number", + "assert_approx_equals", description, + "expectation should be a number, got a ${type_expected}", + {type_expected: typeof expected}); + + assert(actual > expected, + "assert_greater_than", description, + "expected a number greater than ${expected} but got ${actual}", + {expected: expected, actual: actual}); +} +expose(assert_greater_than, 'assert_greater_than'); + +/** Assert that |actual| is less than or equal to |expected|, where + both are primitive numbers. */ +function assert_less_than_equal(actual, expected, description) { + assert(typeof actual === "number", + "assert_less_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual: typeof actual}); + assert(typeof expected === "number", + "assert_approx_equals", description, + "expectation should be a number, got a ${type_expected}", + {type_expected: typeof expected}); + + assert(actual <= expected, + "assert_less_than", description, + "expected a number less than or equal to ${expected} "+ + "but got ${actual}", + {expected: expected, actual: actual}); +} +expose(assert_less_than_equal, 'assert_less_than_equal'); + +/** Assert that |actual| is greater than or equal to |expected|, where + both are primitive numbers. */ +function assert_greater_than_equal(actual, expected, description) { + assert(typeof actual === "number", + "assert_greater_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual: typeof actual}); + assert(typeof expected === "number", + "assert_approx_equals", description, + "expectation should be a number, got a ${type_expected}", + {type_expected: typeof expected}); + + assert(actual >= expected, + "assert_greater_than_equal", description, + "expected a number greater than or equal to ${expected} "+ + "but got ${actual}", + {expected: expected, actual: actual}); +} +expose(assert_greater_than_equal, 'assert_greater_than_equal'); + +/** Assert that |actual|, a string, matches a regexp, |expected|. */ +function assert_regexp_match(actual, expected, description) { + assert(expected.test(actual), + "assert_regexp_match", description, + "expected ${actual} to match ${expected}", + {expected: expected, actual: actual}); +} +expose(assert_regexp_match, 'assert_regexp_match'); + +/** Assert that |actual|, a string, does _not_ match a regexp, |expected|. */ +function assert_regexp_not_match(actual, expected, description) { + assert(!expected.test(actual), + "assert_regexp_not_match", description, + "expected ${actual} not to match ${expected}", + {expected: expected, actual: actual}); +} +expose(assert_regexp_not_match, 'assert_regexp_not_match'); + +/** Assert that |typeof object| is strictly equal to |type|. */ +function assert_type_of(object, type, description) { + assert(typeof object === type, + "assert_type_of", description, + "expected typeof ${object} to be ${expected}, got ${actual}", + {object: object, expected: type, actual: typeof object}); +} +expose(assert_type_of, 'assert_type_of'); + +/** Assert that |object instanceof type|. */ +function assert_instance_of(object, type, description) { + assert(object instanceof type, + "assert_instance_of", description, + "expected ${object} to be instanceof ${expected}", + {object: object, expected: type}); +} +expose(assert_instance_of, 'assert_instance_of'); + +/** Assert that |object| has the class string |expected|. */ +function assert_class_string(object, expected, description) { + var actual = ({}).toString.call(object).slice(8, -1); + assert(actual === expected, + "assert_class_string", description, + "expected ${object} to have class string ${expected}, "+ + "but got ${actual}", + {object: object, expected: expected, actual: actual}); +} +expose(assert_class_string, 'assert_class_string'); + +/** Assert that |object| has a property named |name|. */ +function assert_own_property(object, name, description) { + assert(typeof object === "object", + "assert_own_property", description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + "assert_own_property", description, + "provided value is an object but has no hasOwnProperty method"); + + assert(object.hasOwnProperty(name), + "assert_own_property", description, + "expected property ${name} missing", {name: name}); +} +expose(assert_own_property, 'assert_own_property'); + +/** Assert that |object| inherits a property named |name|. + Note: this assertion will fail for objects that have an + own-property named |name|. */ +function assert_inherits(object, name, description) { + assert(typeof object === "object", + "assert_inherits", description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + "assert_inherits", description, + "provided value is an object but has no hasOwnProperty method"); + + assert(!object.hasOwnProperty(name), + "assert_inherits", description, + "property ${p} found on object, expected only in prototype chain", + {p: name}); + + assert(name in object, + "assert_inherits", description, + "property ${p} not found in prototype chain", + {p: name}); + +} +expose(assert_inherits, 'assert_inherits'); + +/** Assert that |object| neither has nor inherits a property named |name|. */ +function assert_no_property(object, name, description) { + assert(typeof object === "object", + "assert_no_property", description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + "assert_no_property", description, + "provided value is an object but has no hasOwnProperty method"); + + assert(!object.hasOwnProperty(name), + "assert_no_property", description, + "property ${p} found on object, expected to be absent", + {p: name}); + + assert(!(name in object), + "assert_no_property", description, + "property ${p} found in prototype chain, expected to be absent", + {p: name}); +} +expose(assert_no_property, 'assert_no_property'); + +/** Assert that property |name| of |object| is read-only according + to its property descriptor. */ +function assert_readonly(object, name, description) { + var o = {}, desc; + + assert('getOwnPropertyDescriptor' in o, + "assert_readonly", description, + "Object.getOwnPropertyDescriptor is missing"); + + assert(object.hasOwnProperty(name), + "assert_readonly", description, + "expected property ${name} missing", {name: name}); + + desc = o.getOwnPropertyDescriptor.call(object, name); + if ('writable' in desc) { + assert(!desc.writable, "assert_readonly", description, + "data property ${name} is writable (expected read-only)", + {name: name}); + } else { + assert('get' in desc && 'set' in desc, + "assert_readonly", description, + "unrecognized type of property descriptor "+ + "for ${name}: ${desc}", + {name: name, desc: desc}); + assert(desc.set === undefined, + "assert_readonly", description, + "property ${name} has a setter (expected read-only)", + {name: name, desc: desc}); + } +} +expose(assert_readonly, 'assert_readonly'); + +/** Assert that |func| throws an exception described by |code|. + |func| is called with no arguments and no |this| -- use bind() if + that's a problem. |code| can take one of two forms: + + string - the thrown exception must be a DOMException with the + given name, e.g., "TimeoutError", or else it must + stringify to this string. + + object - must have one or more of the properties "code", "name", + and "message". Whichever properties are present must + match the corresponding properties of the thrown + exception. As a special case, "message" will also match + the stringification of the exception. +*/ + +function assert_throws(code, func, description) { + var name_code_map = { + IndexSizeError: 1, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, + + UnknownError: 0, + ConstraintError: 0, + DataError: 0, + TransactionInactiveError: 0, + ReadOnlyError: 0, + VersionError: 0 + }; + + if (typeof code === "object") { + assert("name" in code || "code" in code || "message" in code, + "assert_throws", description, + "exception spec ${code} has no 'name', 'code', or 'message'" + + "properties", + {code: code}); + } else if (name_code_map.hasOwnProperty(code)) { + code = { name: code, + code: name_code_map[code] }; + } else { + code = { message: code.toString() }; + } + + try { + func(); + assert(false, "assert_throws", description, + "${func} did not throw", {func: func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + + // Backward compatibility wart for DOMExceptions identified + // only by numeric code. + if ("code" in code && code.code !== 0 && + (!("name" in e) || e.name === e.name.toUpperCase() || + e.name === "DOMException")) + delete code.name; + + if ("name" in code) { + assert("name" in e && e.name === code.name, + "assert_throws", description, + "${func} threw ${actual} (${actual_name}), "+ + "expected ${expected} (${expected_name})", + {func: func, actual: e, actual_name: e.name, + expected: code, + expected_name: code.name}); + } + if ("code" in code) { + assert("code" in e && e.code === code.code, + "assert_throws", description, + "${func} threw ${actual} (${actual_code}), "+ + "expected ${expected} (${expected_code})", + {func: func, actual: e, actual_code: e.code, + expected: code, + expected_code: code.code}); + } + if ("message" in code) { + if (Object.hasOwnProperty.call(e, "message")) { + assert(e.message === code.message, + "assert_throws", description, + "${func} threw ${actual} (${actual_message}), "+ + "expected ${expected} (${expected_message})", + {func: func, actual: e, actual_message: e.message, + expected: code, expected_message: code.message}); + } else { + // Intentional use of loose equality + assert(e == code.message, + "assert_throws", description, + "${func} threw ${actual}, expected ${expected})", + {func: func, actual: e, expected: code.message}); + } + } + } +} +expose(assert_throws, 'assert_throws'); + +/** Assert that control flow cannot reach the point where this + assertion appears. */ +function assert_unreached(description) { + assert(false, "assert_unreached", description, + "reached unreachable code"); +} +expose(assert_unreached, 'assert_unreached'); + +/** Test object. + * These must be created by calling test() or async_test(), but + * many of their methods are part of the public API. + */ + +function Test(name, properties) { + this.name = name; + this.phase = Test.phases.INITIAL; + this.in_done = false; + this.status = Test.NOTRUN; + this.timeout_id = null; + this.message = null; + this.steps = []; + this.cleanup_callbacks = []; + + if (!properties) { + properties = {}; + } + this.properties = properties; + this.timeout_length = properties.timeout ? properties.timeout + : tests.test_timeout_length; + this.should_run = !properties.skip; + tests.push(this); + this.number = tests.tests.length; + + if (!this.should_run) { + // Fake initial step that _does_ run, but short-circuits the test. + // All other steps will be marked not to be run via the defaults + // in step(). The step function does not call done() because that + // would record a success. We can't do this ourselves because the + // plan line has not been emitted yet. + var stepdata = this.step(function () { + this.phase = Test.phases.COMPLETE; + tests.result(this); + }); + stepdata.should_run = true; + stepdata.auto_run = true; + } +} + +Test.phases = { + INITIAL: 0, + STARTED: 1, + HAS_RESULT: 2, + COMPLETE: 3 +}; +Test.statuses = { + NOTRUN: -1, + PASS: 0, + FAIL: 1, + XFAIL: 4, + XPASS: 5 +}; +Test.prototype.phases = Test.phases; +Test.prototype.statuses = Test.phases; + +(function() { + var x; + var o = Test.statuses; + for (x in o) { + if (o.hasOwnProperty(x)) { + Test[x] = Test.statuses[x]; + Test.prototype[x] = Test.statuses[x]; + } + } +})(); + +/** Queue one step of a test. |func| will eventually be called, with + |this| set to |this_obj|, or to the Test object if |this_obj| is + absent. Any further arguments will be passed down to |func|. It + should carry out some tests using assert_* and eventually return. + |func| will _not_ be called if a previous step of the test has + already failed. + + Returns an object which can be passed to this.perform_step() to + cause |func| actually to be called -- but you should not do this + yourself unless absolutely unavoidable. + */ +Test.prototype.step = function step(func, this_obj) { + if (this_obj == null) { + this_obj = this; + } + func = func.bind(this_obj, Array.prototype.slice.call(arguments, 2)); + + var stepdata = { + func: func, + should_run: this.should_run, + auto_run: this.should_run, + has_run: false + }; + + this.steps.push(stepdata); + return stepdata; +}; + +/** Internal: perform one step of a test. + */ +Test.prototype.perform_step = function perform_step(stepdata) { + var message; + + if (this.phase > this.phases.STARTED || + tests.phase > tests.phases.HAVE_RESULTS) { + return undefined; + } + + this.phase = this.phases.STARTED; + tests.started = true; + stepdata.has_run = true; + + // Arm the local timeout if it hasn't happened already. + if (this.timeout_id === null && this.timeout_length !== null) { + this.timeout_id = setTimeout(this.force_timeout.bind(this), + this.timeout_length); + } + + var rv = undefined; + try { + rv = stepdata.func(); + } catch (e) { + this.fail(format_exception(e)); + } + return rv; +}; + +/** Mark this test as failed. */ +Test.prototype.fail = function fail(message) { + if (this.phase < this.phases.HAS_RESULT) { + this.message = message; + this.status = this.FAIL; + this.phase = this.phases.HAS_RESULT; + if (!this.in_done) { + this.done(); + } + } else { + tests.output.error(message); + } +}; + +/** Mark this test as completed. */ +Test.prototype.done = function done() { + var i; + + this.in_done = true; + + if (this.timeout_id !== null) { + clearTimeout(this.timeout_id); + this.timeout_id = null; + } + + if (this.phase == this.phases.COMPLETE) { + return; + } + + // Cleanups run in reverse order (most recently added first). + for (i = this.cleanup_callbacks.length - 1; i >= 0; i--) { + try { + this.cleanup_callbacks[i].call(this); + } catch (e) { + this.fail("In cleanup: " + format_exception(e)); + } + } + + // If any step of the test was not run (except those that are not + // _supposed_ to run), and no previous error was detected, that is + // an error. + if (this.phase < this.phases.HAS_RESULT) { + for (i = 0; i < this.steps.length; i++) { + if (this.steps[i].should_run && !this.steps[i].has_run) { + this.fail("Step "+i+" was not run"); + } else if (!this.steps[i].should_run && this.steps[i].has_run) { + this.fail("Step "+i+" should not have run"); + } + } + } + + if (this.phase == this.phases.STARTED) { + this.message = null; + this.status = this.PASS; + } + + if (this.properties.expected_fail) { + if (this.status === this.PASS) { + this.status = this.XPASS; + } else if (this.status === this.FAIL) { + this.status = this.XFAIL; + } + } + + this.phase = this.phases.COMPLETE; + + tests.result(this); + +}; + +/** Register |func| as a step function, and return a function that + will run |func|'s step when called. The arguments to |func| are + whatever the arguments to the callback were, and |this| is + |this_obj|, which defaults to the Test object. Useful as an event + handler, for instance. */ +Test.prototype.step_func = function(func, this_obj) { + var test_this = this; + var cb_args = []; + if (arguments.length === 1) { + this_obj = test_this; + } + + // The function returned stashes its arguments in |cb_args|, then + // the registered step function uses them to call |func| with the + // appropriate arguments. We have to do it this way because + // perform_step() doesn't forward its arguments. + var stepdata = this.step(function cb_step () { + return func.apply(this_obj, cb_args); + }); + + stepdata.auto_run = false; + return function() { + cb_args = Array.prototype.slice.call(arguments); + return test_this.perform_step(stepdata); + }; +}; + +/** As |step_func|, but the step calls this.done() after |func| + returns (regardless of what it returns). |func| may be omitted, + in which case the step just calls this.done(). */ +Test.prototype.step_func_done = function(func, this_obj) { + var test_this = this; + var cb_args = []; + + if (arguments.length <= 1) { + this_obj = test_this; + } + if (!func) { + func = function () {}; + } + + // The function returned stashes its arguments in |cb_args|, then + // the registered step function uses them to call |func| with the + // appropriate arguments. We have to do it this way because + // perform_step() doesn't forward its arguments. + var stepdata = this.step(function cb_done_step () { + var rv = func.apply(this_obj, cb_args); + test_this.done(); + return rv; + }); + + stepdata.auto_run = false; + return function() { + cb_args = Array.prototype.slice.call(arguments); + return test_this.perform_step(stepdata); + }; +}; + +/** Returns a function that, if called, will call assert_unreached() + inside a perform_step() invocation. Use to set event handlers for + events that should _not_ happen. */ +Test.prototype.unreached_func = function unreached_func(description) { + var test_this = this; + var stepdata = this.step(function unreached_step () { + assert_unreached(description); + }); + stepdata.should_run = false; + stepdata.auto_run = false; + + return function() { test_this.perform_step(stepdata); }; +}; + +/** Register |callback| to be called once this test is done. */ +Test.prototype.add_cleanup = function add_cleanup(callback) { + this.cleanup_callbacks.push(callback); +}; + +/** Treat this test as having timed out. */ +Test.prototype.force_timeout = function force_timeout() { + this.message = "Test timed out"; + this.status = this.FAIL; + this.phase = this.phases.HAS_RESULT; + this.done(); +}; + + +/* + * Private implementation logic begins at this point. + */ + +/* + * The Tests object is responsible for tracking the complete set of + * tests in this file. + */ + +function Tests(output) { + this.tests = []; + this.num_pending = 0; + + this.all_loaded = false; + this.wait_for_finish = false; + this.allow_uncaught_exception = false; + + this.test_timeout_length = settings.test_timeout; + this.harness_timeout_length = settings.harness_timeout; + this.timeout_id = null; + + this.properties = {}; + this.phase = Test.phases.INITIAL; + this.output = output; + + var this_obj = this; + phantom.onError = function (message, stack) { + if (!tests.allow_uncaught_exception) { + this_obj.output.error(message); + } + if (this_obj.all_done()) { + this_obj.complete(); + } + }; + phantom.page.onConsoleMessage = function (message) { + if (!tests.allow_uncaught_exception) { + this_obj.output.error("stray console message: " + message); + } + }; + this.set_timeout(); +} + +Tests.phases = { + INITIAL: 0, + SETUP: 1, + HAVE_TESTS: 2, + HAVE_RESULTS: 3, + ABANDONED: 4, + COMPLETE: 5 +}; +Tests.prototype.phases = Tests.phases; + +Tests.prototype.setup = function setup(func, properties) { + if (this.phase >= this.phases.HAVE_RESULTS) { + return; + } + + if (this.phase < this.phases.SETUP) { + this.phase = this.phases.SETUP; + } + + this.properties = properties; + + for (var p in properties) { + if (properties.hasOwnProperty(p)) { + var value = properties[p]; + if (p == "allow_uncaught_exception") { + this.allow_uncaught_exception = value; + } else if (p == "explicit_done" && value) { + this.wait_for_finish = true; + } else if (p == "timeout" && value) { + this.harness_timeout_length = value; + } else if (p == "test_timeout") { + this.test_timeout_length = value; + } + } + } + this.set_timeout(); + + if (func) { + try { + func(); + } catch (e) { + this.output.error(e); + this.phase = this.phases.ABANDONED; + } + } +}; + +Tests.prototype.set_timeout = function set_timeout() { + var this_obj = this; + clearTimeout(this.timeout_id); + if (this.harness_timeout_length !== null) { + this.timeout_id = setTimeout(function () { this_obj.timeout(); }, + this.harness_timeout_length); + } +}; + +Tests.prototype.timeout = function timeout() { + this.output.error("Global timeout expired"); + for (var i = 0; i < tests.tests.length; i++) { + var t = tests.tests[i]; + if (t.phase < Test.phases.HAS_RESULT) { + t.force_timeout(); + } + } + this.complete(); +}; + +Tests.prototype.end_wait = function end_wait() { + this.wait_for_finish = false; + if (this.all_done()) { + this.complete(); + } +}; + +Tests.prototype.push = function push(test) +{ + if (this.phase < this.phases.HAVE_TESTS) { + this.phase = this.phases.HAVE_TESTS; + } + this.num_pending++; + this.tests.push(test); +}; + +Tests.prototype.all_done = function all_done() { + return (this.tests.length > 0 && + this.all_loaded && + this.num_pending === 0 && + !this.wait_for_finish); +}; + +Tests.prototype.result = function result(test) { + if (this.phase < this.phases.HAVE_RESULTS) { + this.phase = this.phases.HAVE_RESULTS; + } + this.num_pending--; + this.output.result(test); + if (this.all_done()) { + this.complete(); + } else { + setTimeout(this.run_tests.bind(this), 0); + } +}; + +Tests.prototype.run_tests = function run_tests() { + if (this.phase >= this.phases.COMPLETE) { + return; + } + if (this.all_done() || this.phase >= this.phases.ABANDONED) { + this.complete(); + return; + } + for (var i = 0; i < this.tests.length; i++) { + var t = this.tests[i]; + if (t.phase < t.phases.STARTED && t.steps.length > 0) { + for (var j = 0; j < t.steps.length; j++) { + if (t.steps[j].auto_run) { + t.perform_step(t.steps[j]); + } + } + // We will come back here via the setTimeout in + // Tests.prototype.result. + break; + } + } +}; + +Tests.prototype.begin = function begin() { + this.all_loaded = true; + this.output.begin(this.tests.length, this.phase); + this.run_tests(); + if (this.all_done()) { + this.complete(); + } +}; + +Tests.prototype.complete = function complete() { + var i, x; + + if (this.phase === this.phases.COMPLETE) { + return; + } + for (i = 0; i < this.tests.length; i++) { + this.tests[i].done(); + } + + this.phase = this.phases.COMPLETE; + this.output.complete(this); +}; + +/* + * The Output object is responsible for reporting the status of each + * test. For PhantomJS, output is much simpler than for the W3C + * harness; we basically just log things to the console as + * appropriate. The output format is meant to be compatible with + * TAP (http://testanything.org/tap-specification.html). + */ + +function Output(fp, verbose) { + this.fp = fp; + this.verbose = verbose; + this.failed = false; +} + +Output.prototype.begin = function begin(n, phase) { + if (phase === Tests.phases.ABANDONED) { + this.fp.write("1..0 # SKIP: setup failed\n"); + } else { + this.fp.write("1.." + n + "\n"); + } +}; + +Output.prototype.diagnostic = function diagnostic(message, is_info) { + var fp = this.fp; + var prefix = "# "; + if (is_info) { + prefix = "## "; + } + message.split("\n").forEach(function (line) { + fp.write(prefix + line + "\n"); + }); +}; + +Output.prototype.error = function error(message) { + this.diagnostic("ERROR: " + message); + this.failed = true; +}; + +Output.prototype.info = function info(message) { + this.diagnostic(message, true); +}; + +Output.prototype.result = function result(test) { + if (test.message) { + this.diagnostic(test.message); + } + var prefix, directive = ""; + switch (test.status) { + case Test.PASS: prefix = "ok "; break; + case Test.FAIL: prefix = "not ok "; break; + case Test.XPASS: prefix = "ok "; directive = " # TODO"; break; + case Test.XFAIL: prefix = "not ok "; directive = " # TODO"; break; + case Test.NOTRUN: prefix = "ok "; directive = " # SKIP"; break; + default: + this.error("Unrecognized test status " + test.status); + prefix = "not ok "; + } + if ((prefix === "not ok " && directive !== " # TODO") || + (prefix === "ok " && directive === " # TODO")) { + this.failed = true; + } + this.fp.write(prefix + test.number + " " + test.name + directive + "\n"); +}; + +Output.prototype.complete = function complete(tests) { + phantom.exit(this.failed ? 1 : 0); +}; + +/* + * Utilities. + */ + +function expose(fn, name) { + window[name] = fn; +} + +/** This function is not part of the public API, but its + *behavior* is part of the contract of several assert_* functions. */ +function same_value(x, y) { + if (x === y) { + // Distinguish +0 and -0 + if (x === 0 && y === 0) { + return 1/x === 1/y; + } + return true; + } else { + // NaN !== _everything_, including another NaN. + // Make it same_value as itself. + if (x !== x) { + return y !== y; + } + // Compare Date and RegExp by value. + if (x instanceof Date) { + return y instanceof Date && x.getTime() === y.getTime(); + } + if (x instanceof RegExp) { + return y instanceof RegExp && x.toString() === y.toString(); + } + return false; + } +} + +/** Similarly, this function's behavior is part of the contract of + assert_deep_equals. (These are the things for which it will just + call same_value rather than doing a recursive property comparison.) */ +function is_primitive_value(val) { + return (val === undefined || val === null || typeof val !== 'object' || + val instanceof Date || val instanceof RegExp); +} + +var names_used = {}; +function test_name(func, name) { + var n, c; + + if (name) + ; + else if (func && func.displayName) + name = func.displayName; + else if (func && func.name) + name = func.name; + else + name = "test"; + + n = name; + c = 0; + while (n in names_used) { + n = name + "." + c.toString(); + c += 1; + } + return n; +} + +function AssertionError(message) { + this.message = message; +} + +AssertionError.prototype.toString = function toString() { + return this.message; +}; + +function assert(expected_true, name, description, error, substitutions) { + if (expected_true !== true) { + throw new AssertionError(make_message( + name, description, error, substitutions)); + } else if (output.verbose >= 4) { + output.info(make_message(name, description, error, substitutions)); + } +} + +function make_message(function_name, description, error, substitutions) { + var p, message; + + for (p in substitutions) { + if (substitutions.hasOwnProperty(p)) { + substitutions[p] = format_value(substitutions[p]); + } + } + + if (description) { + description += ": "; + } else { + description = ""; + } + + return (function_name + ": " + description + + error.replace(/\$\{[^}]+\}/g, function (match) { + return substitutions[match.slice(2,-1)]; + })); +} + +function format_value(val, seen) { + if (seen === undefined) + seen = []; + + var s; + function truncate(s, len) { + if (s.length > len) { + return s.slice(-3) + "..."; + } + return s; + } + + switch (typeof val) { + case "number": + // In JavaScript, -0 === 0 and String(-0) == "0", so we have to + // special-case. + if (val === -0 && 1/val === -Infinity) { + return "-0"; + } + // falls through + case "boolean": + case "undefined": + return String(val); + + case "string": + // Escape ", \, all C0 and C1 control characters, and + // Unicode's LINE SEPARATOR and PARAGRAPH SEPARATOR. + // The latter two are the only characters above U+009F + // that may not appear verbatim in a JS string constant. + val = val.replace(/["\\\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, + function (c) { + switch (c) { + case "\b": return "\\b"; + case "\f": return "\\f"; + case "\n": return "\\n"; + case "\r": return "\\r"; + case "\t": return "\\t"; + case "\v": return "\\v"; + case "\\": return "\\\\"; + case "\"": return "\\\""; + default: + // We know by construction that c is + // a single BMP character. + c = c.charCodeAt(0); + if (c < 0x0080) { + return "\\x" + + ("00" + c.toString(16)).slice(-2); + } else { + return "\\u" + + ("0000" + c.toString(16)).slice(-4); + } + } + }); + return '"' + val + '"'; + + case "object": + if (val === null) { + return "null"; + } + if (seen.indexOf(val) >= 0) { + return ""; + } + seen.push(val); + + if (Array.isArray(val)) { + return "[" + val.map(function (x) { + return format_value(x, seen); + }).join(", ") + "]"; + } + + s = String(val); + if (s != "[object Object]") { + return truncate(s, 60); + } + return "{ " + Object.keys(val).map(function (k) { + return format_value(k, seen) + ": " + format_value(val[k], seen); + }).join(", ") + "}"; + + default: + return typeof val + ' "' + truncate(String(val), 60) + '"'; + } +} + +function format_exception (e) { + var message = (typeof e === "object" && e !== null) ? e.message : e; + if (typeof e.stack != "undefined" && typeof e.message == "string") { + // Prune the stack. This knows the format of WebKit's stack traces. + var stack = e.stack.split("\n"); + var lo, hi; + // We do not need to hear about initial lines naming the + // assertion function. + for (lo = 0; lo < stack.length; lo++) { + if (!/^assert(?:_[a-z0-9_]+)?@.*?testharness\.js:/ + .test(stack[lo])) { + break; + } + } + // We do not need to hear about how we got _to_ the test function. + // The caller of the test function is guaranteed to have "_step" in + // its name. + for (hi = lo; hi < stack.length; hi++) { + if (/^[a-z_]+_step.*?testharness\.js:/.test(stack[hi])) { + break; + } + } + if (lo < stack.length && lo < hi) { + stack = stack.slice(lo, hi); + } + message += "\n"; + message += stack.join("\n"); + } + return message; +} + +function process_command_line(sys) { + function usage(error) { + sys.stderr.write("error: " + error + "\n"); + sys.stderr.write("usage: " + sys.args[0] + + " [--verbose=N] test_script.js ...\n"); + } + var args = { verbose: -1, + test_script: "" }; + + for (var i = 1; i < sys.args.length; i++) { + if (sys.args[i].length === 0) { + usage("empty argument is not meaningful"); + return args; + } + if (sys.args[i][0] !== '-') { + args.test_script = sys.args[i]; + break; + } + var n = "--verbose=".length; + var v = sys.args[i].slice(0, n); + var a = sys.args[i].slice(n); + if (v === "--verbose=" && /^[0-9]+$/.test(a)) { + if (args.verbose === -1) { + args.verbose = parseInt(a, 10); + continue; + } else { + usage("--verbose specified twice"); + return args; + } + } + usage("unrecognized option " + format_value(sys.args[i])); + return args; + } + + if (args.test_script === "") { + usage("no test script specified"); + return args; + } + + if (args.verbose === -1) { + args.verbose = 0; + } + + return args; +} + +/* + * Global state + */ + +var settings = { + harness_timeout: 5000, + test_timeout: null +}; + +var sys = require('system'); +var fs = require('fs'); +var args = process_command_line(sys); + +if (args.test_script === "") { + // process_command_line has already issued an error message. + phantom.exit(2); +} else { + // Reset the library paths for injectJs and require to the + // directory containing the test script, so relative imports work + // as expected. Unfortunately, phantom.libraryPath is not a + // proper search path -- it can only hold one directory at a time. + // require.paths has no such limitation. + var test_script = fs.absolute(args.test_script); + phantom.libraryPath = test_script.slice(0, + test_script.lastIndexOf(fs.separator)); + require.paths.push(phantom.libraryPath); + + // run-tests.py sets these environment variables to the base URLs + // of its HTTP and HTTPS servers. + expose(sys.env['TEST_HTTP_BASE'], 'TEST_HTTP_BASE'); + expose(sys.env['TEST_HTTPS_BASE'], 'TEST_HTTPS_BASE'); + + var output = new Output(sys.stdout, args.verbose); + var tests = new Tests(output); + + // This evaluates the test script synchronously. + // Any errors should be caught by our onError hook. + phantom.injectJs(test_script); + + tests.begin(); +} + +})(); diff --git a/test/writing-tests.md b/test/writing-tests.md new file mode 100644 index 0000000..1c827fb --- /dev/null +++ b/test/writing-tests.md @@ -0,0 +1,591 @@ +# How to Write Tests for PhantomJS + +PhantomJS's automated tests are `.js` files located in subdirectories +of this directory. The test runner, [`run-tests.py`](run-tests.py), +executes each as a PhantomJS controller script. (Not all +subdirectories of this directory contain tests; the authoritative list +of test subdirectories is in +[the `TESTS` variable in `run-tests.py`](run-tests.py#L26).) + +In addition to all of the usual PhantomJS API, these scripts have +access to a special testing API, loosely based on +[W3C testharness.js](https://github.com/w3c/testharness.js) and +defined in [`testharness.js`](testharness.js) in this directory. They +also have access to HTTP and HTTPS servers on `localhost`, which serve +the files in the [`www`](www) directory. + +## The Structure of Test Scripts + +Test scripts are divided into _subtests_. There are two kinds of +subtest: synchronous and asynchronous. The only difference is that a +synchronous subtest consists of a single JavaScript function (plus +anything it calls) and is considered to be complete when that function +returns. An asynchronous subtest, however, consists of many +JavaScript functions; these functions are referred to as _steps_. One +step will be invoked to start the subtest, and the others are called +in response to events. Eventually one of the steps will indicate that +the subtest is complete. (The single function used for a synchronous +subtest is also referred to as a step, when the difference doesn't +matter.) + +All of the code in a test script should be part of a subtest, or part +of a setup function (see below). You may define helper functions at +top level, so long as they are only ever _called_ from subtests. You +may also initialize global variables at top level if this +initialization cannot possibly fail, e.g. with constant data or with +`require` calls for core PhantomJS modules. + +The testing API is visible to a test script as a collection of global +functions and variables, documented below. + +A subtest is considered to have failed if any of its steps throws a +JavaScript exception, if an `unreached_func` is called, if the +per-test timeout expires, or if `done` is called before all of its +steps have run. It is considered to succeed if `done` is called +(explicitly or implicitly) after all of its steps have run to +completion. Normally, you should use the assertion functions to detect +failure conditions; these ensure that clear diagnostic information is +printed when a test fails. + +Subtests are executed strictly in the order they appear in the file, +even if some of them are synchronous and some are asynchronous. The +order of steps *within* an asynchronous subtest, however, may be +unpredictable. Also, subtests do not begin to execute until after all +top-level code in the file has been evaluated. + +Some anomalous conditions are reported not as a failure, but as an +"error". For instance, if PhantomJS crashes during a test run, or if +an exception is thrown from outside a step, that is an error, and the +entire test will be abandoned. + +**WARNING:** The harness will become confused if any function passed +as the `func` argument to `test`, `async_test`, `generate_tests`, +`Test.step`, `Test.step_func`, `Test.step_func_done`, or +`Test.add_cleanup` has a name that ends with `_step`. + +### Accessing the HTTP and HTTPS Test Servers + +The global variables `TEST_HTTP_BASE` and `TEST_HTTPS_BASE` are the +base URLs of the test HTTP and HTTPS servers, respectively. Their +values are guaranteed to match the regex `/https?:\/\/localhost:[0-9]+\//`, +but the port number is dynamically assigned for each test run, so you +must not hardwire it. + +### Synchronous Subtests + +There are two functions for defining synchronous subtests. + +* `test(func, name, properties)` + + Run `func` as a subtest; the subtest is considered to be complete as + soon as it returns. `name` is an optional descriptive name for the + subtest. `func` will be called with no arguments, and `this` set to + a `Test` object (see below). `properties` is an optional object + containing properties to apply to the test. Currently there are + three meaningful properties; any other keys in the `properties` + object are ignored: + + * `timeout` - Maximum amount of time the subtest is allowed to run, in + milliseconds. If the timeout expires, the subtest will be + considered to have failed. + + * `expected_fail`: If truthy, this subtest is expected to fail. It will + still be run, but a failure will be considered "normal" and the + overall test will be reported as successful. Conversely, if the + subtest *succeeds* that is considered "abnormal" and the overall + test will be reported as failing. + + * `skip`: If truthy, this subtest will not be run at all, will be + reported as "skipped" rather than "succeeded" or "failed", and + will not affect the overall outcome of the test. Use `skip` only + when `expected_fail` will not do—for instance, when a test + provokes a PhantomJS crash (there currently being no way to label + a crash as "expected"). + + `test` returns the same `Test` object that is available as `this` + within `func`. + +* `generate_tests(func, args, properties)` + + Define a group of synchronous subtests, each of which will call + `func`, but with different arguments. This is easiest to explain by + example: + + generate_tests(assert_equals, [ + ["Sum one and one", 1 + 1, 2], + ["Sum one and zero", 1 + 0, 1] + ]); + + ... is equivalent to ... + + test(function() {assert_equals(1+1, 2)}, "Sum one and one"); + test(function() {assert_equals(1+0, 1)}, "Sum one and zero"); + + Of course `func` may be as complicated as you like, and there is no + limit either to the number of arguments passed to each subtest, or + to the number of subtests. + + The `properties` argument may be a single properties object, which + will be applied uniformly to all the subtests, or an array of the + same length as `args`, containing appropriate property objects for + each subtest. + + `generate_tests` returns no value. + +### Asynchronous Subtests + +An asynchronous subtest consists of one or more _step_ functions, and +unlike a synchronous subtest, it is not considered to be complete until +the `done` method is called on its `Test` object. When this happens, +if any of the step functions have not been executed, the subtest is a +failure. + +Asynchronous subtests are defined with the `async_test` function, which +is almost the same as `test`: + +* `async_test(func, name, properties)` + + Define an asynchronous subtest. The arguments and their + interpretation are the same as for `test`, except that `func` is + optional, and the subtest is *not* considered to be complete after + `func` returns; `func` (if present) is only the first step of the + subtest. Additional steps may be defined, either within `func` or + outside the call to `async_test`, by use of methods on the `Test` + object that is returned (and available as `this` within `func`). + + Normally, an asynchronous subtest's first step will set up whatever + is being tested, and define the remainder of the steps, which will + be run in response to events. + +### Test Object Methods + +These methods are provided by the `Test` object which is returned by +`test` and `async_test`, and available as `this` within step +functions. + +* `Test.step(func[, this_obj, ...])` + + Queue one step of a subtest. `func` will eventually be called, with + `this` set to `this_obj`, or (if `this_obj` is null or absent) to + the `Test` object. Any further arguments will be passed down to + `func`. `func` will _not_ be called if a previous step has failed. + + Do not use this function to define steps that should run in response + to event callbacks; only steps that should be automatically run by + the test harness. + + The object returned by this function is private. Please let us know + if you think you need to use it. + +* `Test.done()` + + Indicate that this subtest is complete. One, and only one, step of + an asynchronous subtest must call this function, or the subtest will + never complete (and eventually it will time out). + + If an asynchronous subtest has several steps, but not all of them + have run when `done` is called, the subtest is considered to have + failed. + +* `Test.step_func(func[, this_obj])` + + Register `func` as a step that will *not* be run automatically by + the test harness. Instead, the function *returned* by this function + (the "callback") will run `func`'s step when it is called. + (`func`'s step must still somehow get run before `done` is called, + or the subtest will be considered to have failed.) + + `this_obj` will be supplied as `this` to `func`; if omitted, it + defaults to the `Test` object. Further arguments are ignored. + However, `func` will receive all of the arguments passed to the + callback, and the callback will return whatever `func` returns. + + This is the normal way to register a step that should run in + response to an event. For instance, here is a minimal test of a + page load: + + async_test(function () { + var p = require('webpage').create(); + p.open(TEST_HTTP_BASE + 'hello.html', + this.step_func(function (status) { + assert_equals(status, 'success'); + this.done(); + })); + }); + + This also serves to illustrate why asynchronous subtests may be + necessary: this subtest is not complete when its first step returns, + only when the `onLoadFinished` event fires. + +* `Test.step_func_done([func[, this_obj]])` + + Same as `Test.step_func`, but the callback additionally calls `done` + after `func` returns. `func` may be omitted, in which case the + callback just calls `done`. + + The example above can be shortened to + + async_test(function () { + var p = require('webpage').create(); + p.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + })); + }); + +* `Test.unreached_func([description])` + + Returns a function that, if called, will call + `assert_unreached(description)` inside a step. Use this to set + event handlers for events that should _not_ happen. You need to use + this method instead of `step_func(function () { assert_unreached(); })` + so the step is properly marked as expected _not_ to run; otherwise + the test will fail whether or not the event happens. + + A slightly more thorough test of a page load might read + + async_test(function () { + var p = require('webpage').create(); + p.onResourceError = this.unreached_func("onResourceError"); + p.open(TEST_HTTP_BASE + 'hello.html', + this.step_func_done(function (status) { + assert_equals(status, 'success'); + })); + }); + +* `Test.add_cleanup(func)` + + Register `func` to be called (with no arguments, and `this` set to + the `Test` object) when `done` is called, whether or not the subtest + has failed. Use this to deallocate persistent resources or undo + changes to global state. For example, a subtest that uses a scratch + file might read + + test(function () { + var fs = require('fs'); + var f = fs.open('scratch_file', 'w'); + this.add_cleanup(function () { + f.close(); + fs.remove('scratch_file'); + }); + + // ... more test logic here ... + }); + + If the step function had simply deleted the file at its end, the + file would only get deleted when the test succeeds. This example + could be rewritten using `try ... finally ...`, but that will not + work for asynchronous tests. + +* `Test.fail(message)` + + Explicitly mark this subtest has having failed, with failure message + `message`. You should not normally need to call this function yourself. + +* `Test.force_timeout()` + + Explicitly mark this subtest as having failed because its timeout has + expired. You should not normally need to call this function yourself. + +### Test Script-Wide Setup + +All of the subtests of a test script are normally run, even if one of +them fails. Complex tests may involve complex initialization actions +that may fail, in which case the entire test script should be aborted. +There is also a small amount of harness-wide configuration that is +possible. Both these tasks are handled by the global function +`setup`. + +* `setup([func], [properties])` + + One may specify either `func` or `properties` or both, but if both + are specified, `func` must be first. + + `func` is called immediately, with no arguments. If it throws an + exception, the entire test script is considered to have failed and + none of the subtests are run. + + `properties` is an object containing one or more of the following + keys: + + * `explicit_done`: Wait for the global function `done` (not to be + confused with `Test.done` to be called, before declaring the test + script complete. + + * `allow_uncaught_exception`: Don't treat an uncaught exception from + non-test code as an error. (Exceptions thrown out of test steps + are still errors.) + + * `timeout`: Overall timeout for the test script, in milliseconds. + The default is five seconds. Note that `run-tests.py` imposes a + "backstop" timeout itself; if you raise this timeout you may also + need to raise that one. + + * `test_timeout`: Default timeout for individual subtests. This may + be overridden by the `timeout` property on a specific subtest. + The default is not to have a timeout for individual subtests. + + +### Assertions + +Whenever possible, use these functions to detect failure conditions. +All of them either throw a JavaScript exception (when the test fails) +or return no value (when the test succeeds). All take one or more +values to be tested, and an optional _description_. If present, the +description should be a string to be printed to clarify why the test +has failed. (The assertions all go to some length to print out values +that were not as expected in a clear format, so descriptions will +often not be necessary.) + +* `assert_is_true(value[, description])` + + `value` must be strictly equal to `true`. + +* `assert_is_false(value[, description])` + + `value` must be strictly equal to `false`. + +* `assert_equals(actual, expected[, description])` + + `actual` and `expected` must be shallowly, strictly equal. The + criterion used is `===` with the following exceptions: + + * If `x === y`, but one of them is `+0` and the other is `-0`, they + are *not* considered equal. + + * If `x !== y`, but one of the following cases holds, they *are* + considered equal: + * both are `NaN` + * both are `Date` objects and `x.getTime() === y.getTime()` + * both are `RegExp` objects and `x.toString() === y.toString()` + +* `assert_not_equals(actual, expected[, description])` + + `actual` and `expected` must *not* be shallowly, strictly equal, + using the same criterion as `assert_equals`. + +* `assert_deep_equals(actual, expected[, description])` + + If `actual` and `expected` are not objects, or if they are + `Date` or `RegExp` objects, this is the same as `assert_equals`. + + Objects that are not `Date` or `RegExp` must have the same set of + own-properties (including non-enumerable own-properties), and each + pair of values for with each own-property must be `deep_equals`, + recursively. Prototype chains are ignored. Back-references are + detected and ignored; they will not cause an infinite recursion. + +* `assert_approx_equals(actual, expected, epsilon[, description])` + + The absolute value of the difference between `actual` and `expected` + must be no greater than `epsilon`. All three arguments must be + primitive numbers. + +* `assert_less_than(actual, expected[, description])` +* `assert_less_than_equal(actual, expected[, description])` +* `assert_greater_than(actual, expected[, description])` +* `assert_greater_than_equal(actual, expected[, description])` + + `actual` and `expected` must be primitive numbers, and `actual` must + be, respectively: less than, less than or equal to, greater than, + greater than or equal to `expected`. + +* `assert_in_array(value, array[, description])` + + `array` must contain `value` according to `Array.indexOf`. + +* `assert_regexp_match(string, regexp[, description])` + + The regular expression `regexp` must match the string `string`, + according to `RegExp.test()`. + +* `assert_regexp_not_match(string, regexp[, description])` + + The regular expression `regexp` must *not* match the string `string`, + according to `RegExp.test()`. + +* `assert_type_of(object, type[, description])` + + `typeof object` must be strictly equal to `type`. + +* `assert_instance_of(object, type[, description])` + + `object instanceof type` must be true. + +* `assert_class_string(object, expected[, description])` + + `object` must have the class string `expected`. The class string is + the second word in the string returned by `Object.prototype.toString`: + for instance, `({}).toString.call([])` returns `[object Array]`, so + `[]`'s class string is `Array`. + +* `assert_own_property(object, name[, description])` + + `object` must have an own-property named `name`. + +* `assert_inherits(object, name[, description])` + + `object` must inherit a property named `name`; that is, + `name in object` must be true but `object.hasOwnProperty(name)` + must be false. + +* `assert_no_property(object, name[, description])` + + `object` must neither have nor inherit a property named `name`. + +* `assert_readonly(object, name[, description])` + + `object` must have an own-property named `name` which is marked + read-only (according to `Object.getOwnPropertyDescriptor`). + +* `assert_throws(code, func[, description])` + + `func` must throw an exception described by `code`. `func` is + called with no arguments and no `this` (you can supply arguments + using `bind`). `code` can take one of two forms. If it is a + string, the thrown exception must either stringify to that string, + or it must be a DOMException whose `name` property is that string. + Otherwise, `code` must be an object with one or more of the + properties `code`, `name`, and `message`; whichever properties are + present must be `===` to the corresponding properties of the + exception. As a special case, if `message` is present in the + `code` object but *not* on the exception object, and the exception + stringifies to the same string as `message`, that's also considered + valid. + + `assert_throws` cannot be used to catch the exception thrown by any + of the other `assert_*` functions when they fail. (You might be + looking for the `expected_fail` property on a subtest.) + +* `assert_unreached([description])` + + Control flow must not reach the point where this assertion appears. + (In other words, this assertion fails unconditionally.) + +## Test Annotations + +Some tests need to be run in a special way. You can indicate this to +the test runner with _annotations_. Annotations are lines in the test +script that begin with the three characters '`//!`'. They must be all +together at the very top of the script; `run-tests.py` stops parsing +at the first line that does _not_ begin with the annotation marker. + +Annotation lines are split into _tokens_ in a shell-like fashion, +which means they are normally separated by whitespace, but you can use +backslashes or quotes to put whitespace inside a token. Backslashes +are significant inside double quotes, but not inside single quotes. +There can be any number of tokens on a line. Everything following an +unquoted, un-backslashed `#` is discarded. (The exact algorithm is +[`shlex.split`](https://docs.python.org/2/library/shlex.html), in +`comments=True`, `posix=True` mode.) + +These are the recognized tokens: + +* `no-harness`: Run this test script directly; the testing API + described above will not be available. This is necessary to + test PhantomJS features that `testharness.js` reserves for its + own use, such as `phantom.onError` and `phantom.exit`. No-harness + tests will usually also be output-expectations tests (see below) + but this is not required. + +* `no-snakeoil`: Do not instruct PhantomJS to accept the self-signed + certificate presented by the HTTPS test server. + +* `timeout:` The next token on the line must be a positive + floating-point number. `run-tests.py` will kill the PhantomJS + process, and consider the test to have failed, if it runs for longer + than that many seconds. The default timeout is seven seconds. + + This timeout is separate from the per-subtest and global timeouts + enforced by the testing API. It is intended as a backstop against + bugs which cause PhantomJS to stop executing the controller script. + (In no-harness mode, it's the only timeout, unless you implement + your own.) + +* `phantomjs:` All subsequent tokens on the line will be passed as + command-line arguments to PhantomJS, before the controller script. + Note that `run-tests.py` sets several PhantomJS command line options + itself; you must take care not to do something contradictory. + +* `script:` All subsequent tokens on the line will be passed as + command-line arguments to the *controller script*; that is, they + will be available in + [`system.args`](http://phantomjs.org/api/system/property/args.html). + Note that your controller script will only be `system.args[0]` if + you are using no-harness mode, and that `run-tests.py` may pass + additional script arguments of its own. + +* `stdin:` All subsequent tokens on the line will be concatenated + (separated by a single space) and fed to PhantomJS's standard input, + with a trailing newline. If this token is used more than once, + that produces several lines of input. If this token is not used at + all, standard input will read as empty. + +## Output-Expectations Tests + +Normally, `run-tests.py` expects each test to produce parseable output +in the [TAP](http://testanything.org/tap-specification.html) format. +This is too inflexible for testing things like `system.stdout.write`, +so there is also a mode in which you specify exactly what output the +test should produce, with additional annotations. Output-expectations +tests are not *required* to be no-harness tests, but the only reason +to use this mode for harness tests would be to test the harness +itself, and it's not sophisticated enough for that. + +Using any of the following annotations makes a test an +output-expectations test: + +* `expect-exit:` The next token on the line must be an integer. If it + is nonnegative, the PhantomJS process is expected to exit with that + exit code. If it is negative, the process is expected to be + terminated by the signal whose number is the absolute value of the + token. (For instance, `expect-exit: -15` for a test that is + expected to hit the backstop timeout.) + +* `expect-stdout:` All subsequent tokens on the line are concatenated, + with spaces in between, and a newline is appeneded. The PhantomJS + process is expected to emit that text, verbatim, on its standard + output. If used more than once, that produces multiple lines of + expected output. + +* `expect-stderr:` Same as `expect-stdout`, but the output is expected + to appear on standard error. + +* `expect-exit-fails`, `expect-stdout-fails`, `expect-stderr-fails`: + The corresponding test (of the exit code, stdout, or stderr) is + expected to fail. + +If some but not all of these annotations are used in a test, the +omitted ones default to exit code 0 (success) and no output on their +respective streams. + +## Test Server Modules + +The HTTP and HTTPS servers exposed to the test suite serve the static +files in the `www` subdirectory with URLs corresponding to their paths +relative to that directory. If you need more complicated server +behavior than that, you can write custom Python code that executes +when the server receives a request. Any `.py` file below the `www` +directory will be invoked to provide the response for that path +*without* the `.py` suffix. (For instance, `www/echo.py` provides +responses for `TEST_HTTP_BASE + 'echo'`.) Such files must define a +top-level function named `handle_request`. This function receives a +single argument, which is an instance of a subclass of +[`BaseHTTPServer.BaseHTTPRequestHandler`](https://docs.python.org/2/library/basehttpserver.html#BaseHTTPServer.BaseHTTPRequestHandler). +The request headers and body (if any) may be retrieved from this +object. The function must use the `send_response`, `send_header`, and +`end_headers` methods of this object to generate HTTP response +headers, and then return a *file-like object* (**not** a string) +containing the response body. The function is responsible for +generating appropriate `Content-Type` and `Content-Length` headers; +the server framework does not do this automatically. + +Test server modules cannot directly cause a test to fail; the server +does not know which test is responsible for any given request. If +there is something wrong with a request, generate an HTTP error +response; then write your test to fail if it receives an error +response. + +Python exceptions thrown by test server modules are treated as +failures *of the testsuite*, but they are all attributed to a virtual +"HTTP server errors" test. diff --git a/test/www/__init__.py b/test/www/__init__.py new file mode 100644 index 0000000..b285886 --- /dev/null +++ b/test/www/__init__.py @@ -0,0 +1,2 @@ +# This file makes test/www/ into a "package" so that +# importing Python response hooks works correctly. diff --git a/test/www/delay.py b/test/www/delay.py new file mode 100644 index 0000000..9653499 --- /dev/null +++ b/test/www/delay.py @@ -0,0 +1,15 @@ +import cStringIO as StringIO +import urlparse +import time + +def handle_request(req): + url = urlparse.urlparse(req.path) + delay = float(int(url.query)) + time.sleep(delay / 1000) # argument is in milliseconds + + body = "OK ({}ms delayed)\n".format(delay) + req.send_response(200) + req.send_header('Content-Type', 'text/plain') + req.send_header('Content-Length', str(len(body))) + req.end_headers() + return StringIO.StringIO(body) diff --git a/test/www/echo.py b/test/www/echo.py new file mode 100644 index 0000000..8a7ba2d --- /dev/null +++ b/test/www/echo.py @@ -0,0 +1,29 @@ +import json +import urlparse +import cStringIO as StringIO + +def handle_request(req): + url = urlparse.urlparse(req.path) + headers = {} + for name, value in req.headers.items(): + headers[name] = value.rstrip() + + d = dict( + command = req.command, + version = req.protocol_version, + origin = req.client_address, + url = req.path, + path = url.path, + params = url.params, + query = url.query, + fragment = url.fragment, + headers = headers, + postdata = req.postdata + ) + body = json.dumps(d, indent=2) + '\n' + + req.send_response(200) + req.send_header('Content-Type', 'application/json') + req.send_header('Content-Length', str(len(body))) + req.end_headers() + return StringIO.StringIO(body) diff --git a/test/www/frameset/frame1-1.html b/test/www/frameset/frame1-1.html new file mode 100644 index 0000000..c80ec45 --- /dev/null +++ b/test/www/frameset/frame1-1.html @@ -0,0 +1,8 @@ + + + frame1-1 + + +

index > frame1 > frame1-1

+ + diff --git a/test/www/frameset/frame1-2.html b/test/www/frameset/frame1-2.html new file mode 100644 index 0000000..b0c38d2 --- /dev/null +++ b/test/www/frameset/frame1-2.html @@ -0,0 +1,8 @@ + + + frame1-2 + + +

index > frame1 > frame1-2

+ + diff --git a/test/www/frameset/frame1.html b/test/www/frameset/frame1.html new file mode 100644 index 0000000..b23c274 --- /dev/null +++ b/test/www/frameset/frame1.html @@ -0,0 +1,9 @@ + + + frame1 + + + + + + diff --git a/test/www/frameset/frame2-1.html b/test/www/frameset/frame2-1.html new file mode 100644 index 0000000..2f7c121 --- /dev/null +++ b/test/www/frameset/frame2-1.html @@ -0,0 +1,8 @@ + + + frame2-1 + + +

index > frame2 > frame2-1

+ + diff --git a/test/www/frameset/frame2-2.html b/test/www/frameset/frame2-2.html new file mode 100644 index 0000000..99f603d --- /dev/null +++ b/test/www/frameset/frame2-2.html @@ -0,0 +1,8 @@ + + + frame2-2 + + +

index > frame2 > frame2-2

+ + diff --git a/test/www/frameset/frame2-3.html b/test/www/frameset/frame2-3.html new file mode 100644 index 0000000..b6f1808 --- /dev/null +++ b/test/www/frameset/frame2-3.html @@ -0,0 +1,8 @@ + + + frame2-3 + + +

index > frame2 > frame2-3

+ + diff --git a/test/www/frameset/frame2.html b/test/www/frameset/frame2.html new file mode 100644 index 0000000..d3484bd --- /dev/null +++ b/test/www/frameset/frame2.html @@ -0,0 +1,10 @@ + + + frame2 + + + + + + + diff --git a/test/www/frameset/index.html b/test/www/frameset/index.html new file mode 100644 index 0000000..dbe01bb --- /dev/null +++ b/test/www/frameset/index.html @@ -0,0 +1,9 @@ + + + index + + + + + + diff --git a/test/www/hello.html b/test/www/hello.html new file mode 100644 index 0000000..ee4bc59 --- /dev/null +++ b/test/www/hello.html @@ -0,0 +1,8 @@ + + + Hello + + +

Hello, world!

+ + diff --git a/test/www/iframe.html b/test/www/iframe.html new file mode 100644 index 0000000..ce62c6d --- /dev/null +++ b/test/www/iframe.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/test/www/includejs.js b/test/www/includejs.js new file mode 100644 index 0000000..5c0e4bd --- /dev/null +++ b/test/www/includejs.js @@ -0,0 +1,3 @@ +function getTitle () { + return document.title; +} diff --git a/test/www/includejs1.html b/test/www/includejs1.html new file mode 100644 index 0000000..4be4b1f --- /dev/null +++ b/test/www/includejs1.html @@ -0,0 +1,2 @@ + +i am includejs one diff --git a/test/www/includejs2.html b/test/www/includejs2.html new file mode 100644 index 0000000..89aeab2 --- /dev/null +++ b/test/www/includejs2.html @@ -0,0 +1,2 @@ + +i am includejs two diff --git a/test/www/js-infinite-loop.html b/test/www/js-infinite-loop.html new file mode 100644 index 0000000..6dcb775 --- /dev/null +++ b/test/www/js-infinite-loop.html @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/test/www/logo.html b/test/www/logo.html new file mode 100644 index 0000000..1323ffd --- /dev/null +++ b/test/www/logo.html @@ -0,0 +1,8 @@ + + + Show logo + + + + + diff --git a/test/www/logo.png b/test/www/logo.png new file mode 100644 index 0000000..7b44e54 Binary files /dev/null and b/test/www/logo.png differ diff --git a/test/www/missing-img.html b/test/www/missing-img.html new file mode 100644 index 0000000..cc142ec --- /dev/null +++ b/test/www/missing-img.html @@ -0,0 +1,8 @@ + + + Missing image + + + + + diff --git a/test/www/navigation/dest.html b/test/www/navigation/dest.html new file mode 100644 index 0000000..e336f9c --- /dev/null +++ b/test/www/navigation/dest.html @@ -0,0 +1,2 @@ + +DEST diff --git a/test/www/navigation/index.html b/test/www/navigation/index.html new file mode 100644 index 0000000..9d3b35f --- /dev/null +++ b/test/www/navigation/index.html @@ -0,0 +1,2 @@ + +INDEX diff --git a/test/www/phantomjs.png b/test/www/phantomjs.png new file mode 100644 index 0000000..381a378 Binary files /dev/null and b/test/www/phantomjs.png differ diff --git a/test/www/regression/pjs-10690/font.css b/test/www/regression/pjs-10690/font.css new file mode 100644 index 0000000..959d531 --- /dev/null +++ b/test/www/regression/pjs-10690/font.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'WindsongRegular'; + src: url("Windsong.ttf") format("truetype"); +} +h1 { + font: 90px/98px "WindsongRegular", Arial, sans-serif; +} diff --git a/test/www/regression/pjs-10690/index.html b/test/www/regression/pjs-10690/index.html new file mode 100644 index 0000000..6d4d3cf --- /dev/null +++ b/test/www/regression/pjs-10690/index.html @@ -0,0 +1,9 @@ + + + + + + +

Hello World

+ + diff --git a/test/www/regression/pjs-10690/jquery.js b/test/www/regression/pjs-10690/jquery.js new file mode 100644 index 0000000..f66a95c --- /dev/null +++ b/test/www/regression/pjs-10690/jquery.js @@ -0,0 +1,9441 @@ +/*! + * jQuery JavaScript Library v1.8.2 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: Thu Sep 20 2012 21:13:05 GMT-0400 (Eastern Daylight Time) + */ + +(function( window, undefined ) { +var + // A central reference to the root jQuery(document) + rootjQuery, + + // The deferred used on DOM ready + readyList, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + location = window.location, + navigator = window.navigator, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // Save a reference to some core methods + core_push = Array.prototype.push, + core_slice = Array.prototype.slice, + core_indexOf = Array.prototype.indexOf, + core_toString = Object.prototype.toString, + core_hasOwn = Object.prototype.hasOwnProperty, + core_trim = String.prototype.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source, + + // Used for detecting and trimming whitespace + core_rnotwhite = /\S/, + core_rspace = /\s+/, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // The ready event handler and self cleanup method + DOMContentLoaded = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + } else if ( document.readyState === "complete" ) { + // we're here because readyState === "complete" in oldIE + // which is good enough for us to call the dom ready! + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context && context.nodeType ? context.ownerDocument || context : document ); + + // scripts is true for back-compat + selector = jQuery.parseHTML( match[1], doc, true ); + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + this.attr.call( selector, context, true ); + } + + return jQuery.merge( this, selector ); + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.8.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ), + "slice", core_slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ core_toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // scripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, scripts ) { + var parsed; + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + scripts = context; + context = 0; + } + context = context || document; + + // Single tag + if ( (parsed = rsingleTag.exec( data )) ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] ); + return jQuery.merge( [], + (parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes ); + }, + + parseJSON: function( data ) { + if ( !data || typeof data !== "string") { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && core_rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var name, + i = 0, + length = obj.length, + isObj = length === undefined || jQuery.isFunction( obj ); + + if ( args ) { + if ( isObj ) { + for ( name in obj ) { + if ( callback.apply( obj[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( obj[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in obj ) { + if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var type, + ret = results || []; + + if ( arr != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + type = jQuery.type( arr ); + + if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) { + core_push.call( ret, arr ); + } else { + jQuery.merge( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, + ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + } +}); + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready, 1 ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.split( core_rspace ), function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) { + list.push( arg ); + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + return jQuery.inArray( fn, list ) > -1; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ]( jQuery.isFunction( fn ) ? + function() { + var returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + } : + newDefer[ action ] + ); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] = list.fire + deferred[ tuple[0] ] = list.fire; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + eventName, + i, + isSupported, + clickFn, + div = document.createElement("div"); + + // Preliminary tests + div.setAttribute( "className", "t" ); + div.innerHTML = "
a"; + + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + a.style.cssText = "top:1px;float:left;opacity:.5"; + + // Can't get basic test support + if ( !all || !all.length ) { + return {}; + } + + // First batch of supports tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.5/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: ( document.compatMode === "CSS1Compat" ), + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", clickFn = function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent("onclick"); + div.detachEvent( "onclick", clickFn ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + input.setAttribute( "checked", "checked" ); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: true, + change: true, + focusin: true + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + // Run tests that need a body at doc ready + jQuery(function() { + var container, div, tds, marginDiv, + divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // NOTE: To any future maintainer, we've window.getComputedStyle + // because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = document.createElement("div"); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + container.style.zoom = 1; + } + + // Null elements to avoid leaks in IE + body.removeChild( container ); + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + fragment.removeChild( div ); + all = a = select = opt = input = fragment = div = null; + + return support; +})(); +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + deletedIds: [], + + // Remove at next major release (1.9/2.0) + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery.removeData( elem, type + "queue", true ); + jQuery.removeData( elem, key, true ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, fixSpecified, + rclass = /[\t\r\n]/g, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea|)$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( core_rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var removes, className, elem, c, cl, i, l; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + if ( (value && typeof value === "string") || value === undefined ) { + removes = ( value || "" ).split( core_rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + if ( elem.nodeType === 1 && elem.className ) { + + className = (" " + elem.className + " ").replace( rclass, " " ); + + // loop over each item in the removal list + for ( c = 0, cl = removes.length; c < cl; c++ ) { + // Remove until there is nothing to remove, + while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) { + className = className.replace( " " + removes[ c ] + " " , " " ); + } + } + elem.className = value ? jQuery.trim( className ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( core_rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val, + self = jQuery(this); + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + // Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9 + attrFn: {}, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + + attrNames = value.split( core_rspace ); + + for ( ; i < attrNames.length; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.value = value + "" ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/, + rhoverHack = /(?:^|\s)hover(\.\S+|)\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var t, tns, type, origType, namespaces, origCount, + j, events, special, eventType, handleObj, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, "events", true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType, + type = event.type || event, + namespaces = []; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + for ( old = elem; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old === (elem.ownerDocument || document) ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related, + handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = core_slice.call( arguments ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = []; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + selMatch = {}; + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8) + event.metaKey = !!event.metaKey; + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 – + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === "undefined" ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "_submit_attached" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "_submit_attached", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "_change_attached", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { + +var cachedruns, + assertGetIdNotName, + Expr, + getText, + isXML, + contains, + compile, + sortOrder, + hasDuplicate, + outermostContext, + + baseHasDuplicate = true, + strundefined = "undefined", + + expando = ( "sizcache" + Math.random() ).replace( ".", "" ), + + Token = String, + document = window.document, + docElem = document.documentElement, + dirruns = 0, + done = 0, + pop = [].pop, + push = [].push, + slice = [].slice, + // Use a stripped-down indexOf if a native one is unavailable + indexOf = [].indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + // Augment a function for special use by Sizzle + markFunction = function( fn, value ) { + fn[ expando ] = value == null || value; + return fn; + }, + + createCache = function() { + var cache = {}, + keys = []; + + return markFunction(function( key, value ) { + // Only keep the most recent entries + if ( keys.push( key ) > Expr.cacheLength ) { + delete cache[ keys.shift() ]; + } + + return (cache[ key ] = value); + }, cache ); + }, + + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + + // Regex + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors) + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments not in parens/brackets, + // then attribute selectors and non-pseudos (denoted by :), + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)", + + // For matchExpr.POS and matchExpr.needsContext + pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/, + + rnot = /^:not/, + rsibling = /[\x20\t\r\n\f]*[+~]/, + rendsWithNot = /:not\($/, + + rheader = /h\d/i, + rinputs = /input|select|textarea|button/i, + + rbackslash = /\\(?!\\)/g, + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "POS": new RegExp( pos, "i" ), + "CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" ) + }, + + // Support + + // Used for testing something on an element + assert = function( fn ) { + var div = document.createElement("div"); + + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; + } + }, + + // Check if getElementsByTagName("*") returns only elements + assertTagNameNoComments = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }), + + // Check if getAttribute returns normalized href attributes + assertHrefNotNormalized = assert(function( div ) { + div.innerHTML = ""; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }), + + // Check if attributes should be retrieved by attribute nodes + assertAttributes = assert(function( div ) { + div.innerHTML = ""; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }), + + // Check if getElementsByClassName can be trusted + assertUsableClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = ""; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }), + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + assertUsableName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "
"; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = document.getElementsByName && + // buggy browsers will return fewer than the correct 2 + document.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + document.getElementsByName( expando + 0 ).length; + assertGetIdNotName = !document.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + +// If slice is not available, provide a backup +try { + slice.call( docElem.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + for ( ; (elem = this[i]); i++ ) { + results.push( elem ); + } + return results; + }; +} + +function Sizzle( selector, context, results, seed ) { + results = results || []; + context = context || document; + var match, elem, xml, m, + nodeType = context.nodeType; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( nodeType !== 1 && nodeType !== 9 ) { + return []; + } + + xml = isXML( context ); + + if ( !xml && !seed ) { + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed, xml ); +} + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + return Sizzle( expr, null, null, [ elem ] ).length > 0; +}; + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + } else { + + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } + return ret; +}; + +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +// Element contains another +contains = Sizzle.contains = docElem.contains ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) ); + } : + docElem.compareDocumentPosition ? + function( a, b ) { + return b && !!( a.compareDocumentPosition( b ) & 16 ); + } : + function( a, b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + return false; + }; + +Sizzle.attr = function( elem, name ) { + var val, + xml = isXML( elem ); + + if ( !xml ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( xml || assertAttributes ) { + return elem.getAttribute( name ); + } + val = elem.getAttributeNode( name ); + return val ? + typeof elem[ name ] === "boolean" ? + elem[ name ] ? name : null : + val.specified ? val.value : null : + null; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + // IE6/7 return a modified href + attrHandle: assertHrefNotNormalized ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }, + + find: { + "ID": assertGetIdNotName ? + function( id, context, xml ) { + if ( typeof context.getElementById !== strundefined && !xml ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + } : + function( id, context, xml ) { + if ( typeof context.getElementById !== strundefined && !xml ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }, + + "TAG": assertTagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + var elem, + tmp = [], + i = 0; + + for ( ; (elem = results[i]); i++ ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }, + + "NAME": assertUsableName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }, + + "CLASS": assertUsableClassName && function( className, context, xml ) { + if ( typeof context.getElementsByClassName !== strundefined && !xml ) { + return context.getElementsByClassName( className ); + } + } + }, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( rbackslash, "" ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 3 xn-component of xn+y argument ([+-]?\d*n|) + 4 sign of xn-component + 5 x of xn-component + 6 sign of y-component + 7 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1] === "nth" ) { + // nth-child requires argument + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) ); + match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" ); + + // other types prohibit arguments + } else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var unquoted, excess; + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + if ( match[3] ) { + match[2] = match[3]; + } else if ( (unquoted = match[4]) ) { + // Only check arguments that contain a pseudo + if ( rpseudo.test(unquoted) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + unquoted = unquoted.slice( 0, excess ); + match[0] = match[0].slice( 0, excess ); + } + match[2] = unquoted; + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + "ID": assertGetIdNotName ? + function( id ) { + id = id.replace( rbackslash, "" ); + return function( elem ) { + return elem.getAttribute("id") === id; + }; + } : + function( id ) { + id = id.replace( rbackslash, "" ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === id; + }; + }, + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + nodeName = nodeName.replace( rbackslash, "" ).toLowerCase(); + + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ expando ][ className ]; + if ( !pattern ) { + pattern = classCache( className, new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)") ); + } + return function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }; + }, + + "ATTR": function( name, operator, check ) { + return function( elem, context ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.substr( result.length - check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, argument, first, last ) { + + if ( type === "nth" ) { + return function( elem ) { + var node, diff, + parent = elem.parentNode; + + if ( first === 1 && last === 0 ) { + return true; + } + + if ( parent ) { + diff = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + diff++; + if ( elem === node ) { + break; + } + } + } + } + + // Incorporate the offset (or cast to NaN), then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + }; + } + + return function( elem ) { + var node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + var nodeType; + elem = elem.firstChild; + while ( elem ) { + if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) { + return false; + } + elem = elem.nextSibling; + } + return true; + }, + + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "text": function( elem ) { + var type, attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + (type = elem.type) === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type ); + }, + + // Input types + "radio": createInputPseudo("radio"), + "checkbox": createInputPseudo("checkbox"), + "file": createInputPseudo("file"), + "password": createInputPseudo("password"), + "image": createInputPseudo("image"), + + "submit": createButtonPseudo("submit"), + "reset": createButtonPseudo("reset"), + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "focus": function( elem ) { + var doc = elem.ownerDocument; + return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href); + }, + + "active": function( elem ) { + return elem === elem.ownerDocument.activeElement; + }, + + // Positional types + "first": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = 0; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = 1; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +function siblingCheck( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; +} + +sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + return ( !a.compareDocumentPosition || !b.compareDocumentPosition ? + a.compareDocumentPosition : + a.compareDocumentPosition(b) & 4 + ) ? -1 : 1; + } : + function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + +// Always assume the presence of duplicates if sort doesn't +// pass them to our comparison function (as in Google Chrome). +[0, 0].sort( sortOrder ); +baseHasDuplicate = !hasDuplicate; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + i = 1; + + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + + return results; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, soFar, groups, preFilters, + cached = tokenCache[ expando ][ selector ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + soFar = soFar.slice( match[0].length ); + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + tokens.push( matched = new Token( match.shift() ) ); + soFar = soFar.slice( matched.length ); + + // Cast descendant combinators to space + matched.type = match[0].replace( rtrim, " " ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + // The last two arguments here are (context, xml) for backCompat + (match = preFilters[ type ]( match, document, true ))) ) { + + tokens.push( matched = new Token( match.shift() ) ); + soFar = soFar.slice( matched.length ); + matched.type = type; + matched.matches = match; + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && combinator.dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( !xml ) { + var cache, + dirkey = dirruns + " " + doneName + " ", + cachedkey = dirkey + cachedruns; + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + if ( (cache = elem[ expando ]) === cachedkey ) { + return elem.sizset; + } else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) { + if ( elem.sizset ) { + return elem; + } + } else { + elem[ expando ] = cachedkey; + if ( matcher( elem, context, xml ) ) { + elem.sizset = true; + return elem; + } + elem.sizset = false; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( checkNonElements || elem.nodeType === 1 ) { + if ( matcher( elem, context, xml ) ) { + return elem; + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + // Positional selectors apply to seed elements, so it is invalid to follow them with relative ones + if ( seed && postFinder ) { + return; + } + + var i, elem, postFilterIn, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [], seed ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + postFilterIn = condense( matcherOut, postMap ); + postFilter( postFilterIn, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = postFilterIn.length; + while ( i-- ) { + if ( (elem = postFilterIn[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + // Keep seed and results synchronized + if ( seed ) { + // Ignore postFinder because it can't coexist with seed + i = preFilter && matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + seed[ preMap[i] ] = !(results[ preMap[i] ] = elem); + } + } + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + // The concatenated values are (context, xml) for backCompat + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && tokens.join("") + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Nested matchers should use non-integer dirruns + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = superMatcher.el; + } + + // Add elements passing elementMatchers directly to results + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + for ( j = 0; (matcher = elementMatchers[j]); j++ ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++superMatcher.el; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + for ( j = 0; (matcher = setMatchers[j]); j++ ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + superMatcher.el = 0; + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ expando ][ selector ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results, seed ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results, seed ); + } + return results; +} + +function select( selector, context, results, seed, xml ) { + var i, tokens, token, type, find, + match = tokenize( selector ), + j = match.length; + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !xml && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().length ); + } + + // Fetch a seed set for right-to-left matching + for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( rbackslash, "" ), + rsibling.test( tokens[0].type ) && context.parentNode || context, + xml + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && tokens.join(""); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + xml, + results, + rsibling.test( selector ) + ); + return results; +} + +if ( document.querySelectorAll ) { + (function() { + var disconnectedMatch, + oldSelect = select, + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, + + // qSa(:focus) reports false when true (Chrome 21), + // A support test would require too much code (would include document ready) + rbuggyQSA = [":focus"], + + // matchesSelector(:focus) reports false when true (Chrome 21), + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + // A support test would require too much code (would include document ready) + // just skip matchesSelector for :active + rbuggyMatches = [ ":active", ":focus" ], + matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector; + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here (do not put tests after this one) + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE9 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = "

"; + if ( div.querySelectorAll("[test^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here (do not put tests after this one) + div.innerHTML = ""; + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push(":enabled", ":disabled"); + } + }); + + // rbuggyQSA always contains :focus, so no need for a length check + rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") ); + + select = function( selector, context, results, seed, xml ) { + // Only use querySelectorAll when not filtering, + // when this is not xml, + // and when no QSA bugs apply + if ( !seed && !xml && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + var groups, i, + old = true, + nid = expando, + newContext = context, + newSelector = context.nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + groups[i].join(""); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + + return oldSelect( selector, context, results, seed, xml ); + }; + + if ( matches ) { + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + try { + matches.call( div, "[test!='']:sizzle" ); + rbuggyMatches.push( "!=", pseudos ); + } catch ( e ) {} + }); + + // rbuggyMatches always contains :active and :focus, so no need for a length check + rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") ); + + Sizzle.matchesSelector = function( elem, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyMatches always contains :active, so no need for an existence check + if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && (!rbuggyQSA || !rbuggyQSA.test( expr )) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, null, null, [ elem ] ).length > 0; + }; + } + })(); +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Back-compat +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, l, length, n, r, ret, + self = this; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + ret = this.pushStack( "", "find", selector ); + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + cur = this[i]; + + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + } + cur = cur.parentNode; + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +jQuery.fn.andSelf = jQuery.fn.addBack; + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( this.length > 1 && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, core_slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + rcheckableType = /^(?:checkbox|radio)$/, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*\s*$/g, + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, +// unless wrapped in a div with non-breaking characters in front of it. +if ( !jQuery.support.htmlSerialize ) { + wrapMap._default = [ 1, "X
", "
" ]; +} + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + }, + + append: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 ) { + this.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 ) { + this.insertBefore( elem, this.firstChild ); + } + }); + }, + + before: function() { + if ( !isDisconnected( this[0] ) ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this ); + }); + } + + if ( arguments.length ) { + var set = jQuery.clean( arguments ); + return this.pushStack( jQuery.merge( set, this ), "before", this.selector ); + } + }, + + after: function() { + if ( !isDisconnected( this[0] ) ) { + return this.domManip(arguments, false, function( elem ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + } + + if ( arguments.length ) { + var set = jQuery.clean( arguments ); + return this.pushStack( jQuery.merge( this, set ), "after", this.selector ); + } + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + jQuery.cleanData( [ elem ] ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName("*") ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( elem.getElementsByTagName( "*" ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function( value ) { + if ( !isDisconnected( this[0] ) ) { + // Make sure that the elements are removed from the DOM before they are inserted + // this can help fix replacing a parent with child elements + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this), old = self.html(); + self.replaceWith( value.call( this, i, old ) ); + }); + } + + if ( typeof value !== "string" ) { + value = jQuery( value ).detach(); + } + + return this.each(function() { + var next = this.nextSibling, + parent = this.parentNode; + + jQuery( this ).remove(); + + if ( next ) { + jQuery(next).before( value ); + } else { + jQuery(parent).append( value ); + } + }); + } + + return this.length ? + this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : + this; + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, table, callback ) { + + // Flatten any nested arrays + args = [].concat.apply( [], args ); + + var results, first, fragment, iNoClone, + i = 0, + value = args[0], + scripts = [], + l = this.length; + + // We can't cloneNode fragments that contain checked, in WebKit + if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) { + return this.each(function() { + jQuery(this).domManip( args, table, callback ); + }); + } + + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + args[0] = value.call( this, i, table ? self.html() : undefined ); + self.domManip( args, table, callback ); + }); + } + + if ( this[0] ) { + results = jQuery.buildFragment( args, this, scripts ); + fragment = results.fragment; + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + table = table && jQuery.nodeName( first, "tr" ); + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + // Fragments from the fragment cache must always be cloned and never used in place. + for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) { + callback.call( + table && jQuery.nodeName( this[i], "table" ) ? + findOrAppend( this[i], "tbody" ) : + this[i], + i === iNoClone ? + fragment : + jQuery.clone( fragment, true, true ) + ); + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + + if ( scripts.length ) { + jQuery.each( scripts, function( i, elem ) { + if ( elem.src ) { + if ( jQuery.ajax ) { + jQuery.ajax({ + url: elem.src, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } else { + jQuery.error("no ajax"); + } + } else { + jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + + if ( elem.parentNode ) { + elem.parentNode.removeChild( elem ); + } + }); + } + } + + return this; + } +}); + +function findOrAppend( elem, tag ) { + return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function cloneFixAttributes( src, dest ) { + var nodeName; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + // clearAttributes removes the attributes, which we don't want, + // but also removes the attachEvent events, which we *do* want + if ( dest.clearAttributes ) { + dest.clearAttributes(); + } + + // mergeAttributes, in contrast, only merges back on the + // original attributes, not the events + if ( dest.mergeAttributes ) { + dest.mergeAttributes( src ); + } + + nodeName = dest.nodeName.toLowerCase(); + + if ( nodeName === "object" ) { + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + + // IE blanks contents when cloning scripts + } else if ( nodeName === "script" && dest.text !== src.text ) { + dest.text = src.text; + } + + // Event data gets referenced instead of copied if the expando + // gets copied too + dest.removeAttribute( jQuery.expando ); +} + +jQuery.buildFragment = function( args, context, scripts ) { + var fragment, cacheable, cachehit, + first = args[ 0 ]; + + // Set context from what may come in as undefined or a jQuery collection or a node + // Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 & + // also doubles as fix for #8950 where plain objects caused createDocumentFragment exception + context = context || document; + context = !context.nodeType && context[0] || context; + context = context.ownerDocument || context; + + // Only cache "small" (1/2 KB) HTML strings that are associated with the main document + // Cloning options loses the selected state, so don't cache them + // IE 6 doesn't like it when you put or elements in a fragment + // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache + // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 + if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document && + first.charAt(0) === "<" && !rnocache.test( first ) && + (jQuery.support.checkClone || !rchecked.test( first )) && + (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { + + // Mark cacheable and look for a hit + cacheable = true; + fragment = jQuery.fragments[ first ]; + cachehit = fragment !== undefined; + } + + if ( !fragment ) { + fragment = context.createDocumentFragment(); + jQuery.clean( args, context, fragment, scripts ); + + // Update the cache, but only store false + // unless this is a second parsing of the same content + if ( cacheable ) { + jQuery.fragments[ first ] = cachehit && fragment; + } + } + + return { fragment: fragment, cacheable: cacheable }; +}; + +jQuery.fragments = {}; + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + l = insert.length, + parent = this.length === 1 && this[0].parentNode; + + if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) { + insert[ original ]( this[0] ); + return this; + } else { + for ( ; i < l; i++ ) { + elems = ( i > 0 ? this.clone(true) : this ).get(); + jQuery( insert[i] )[ original ]( elems ); + ret = ret.concat( elems ); + } + + return this.pushStack( ret, name, insert.selector ); + } + }; +}); + +function getAll( elem ) { + if ( typeof elem.getElementsByTagName !== "undefined" ) { + return elem.getElementsByTagName( "*" ); + + } else if ( typeof elem.querySelectorAll !== "undefined" ) { + return elem.querySelectorAll( "*" ); + + } else { + return []; + } +} + +// Used in clean, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var srcElements, + destElements, + i, + clone; + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + // IE copies events bound via attachEvent when using cloneNode. + // Calling detachEvent on the clone will also remove the events + // from the original. In order to get around this, we use some + // proprietary methods to clear the events. Thanks to MooTools + // guys for this hotness. + + cloneFixAttributes( elem, clone ); + + // Using Sizzle here is crazy slow, so we use getElementsByTagName instead + srcElements = getAll( elem ); + destElements = getAll( clone ); + + // Weird iteration because IE will replace the length property + // with an element if you are cloning the body and one of the + // elements on the page has a name or id of "length" + for ( i = 0; srcElements[i]; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + cloneFixAttributes( srcElements[i], destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + cloneCopyEvent( elem, clone ); + + if ( deepDataAndEvents ) { + srcElements = getAll( elem ); + destElements = getAll( clone ); + + for ( i = 0; srcElements[i]; ++i ) { + cloneCopyEvent( srcElements[i], destElements[i] ); + } + } + } + + srcElements = destElements = null; + + // Return the cloned set + return clone; + }, + + clean: function( elems, context, fragment, scripts ) { + var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags, + safe = context === document && safeFragment, + ret = []; + + // Ensure that context is a document + if ( !context || typeof context.createDocumentFragment === "undefined" ) { + context = document; + } + + // Use the already-created safe fragment if context permits + for ( i = 0; (elem = elems[i]) != null; i++ ) { + if ( typeof elem === "number" ) { + elem += ""; + } + + if ( !elem ) { + continue; + } + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + if ( !rhtml.test( elem ) ) { + elem = context.createTextNode( elem ); + } else { + // Ensure a safe container in which to render the html + safe = safe || createSafeFragment( context ); + div = context.createElement("div"); + safe.appendChild( div ); + + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(rxhtmlTag, "<$1>"); + + // Go to html and back, then peel off extra wrappers + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + depth = wrap[0]; + div.innerHTML = wrap[1] + elem + wrap[2]; + + // Move to the right depth + while ( depth-- ) { + div = div.lastChild; + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + hasBody = rtbody.test(elem); + tbody = tag === "table" && !hasBody ? + div.firstChild && div.firstChild.childNodes : + + // String was a bare or + wrap[1] === "
" && !hasBody ? + div.childNodes : + []; + + for ( j = tbody.length - 1; j >= 0 ; --j ) { + if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { + tbody[ j ].parentNode.removeChild( tbody[ j ] ); + } + } + } + + // IE completely kills leading whitespace when innerHTML is used + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); + } + + elem = div.childNodes; + + // Take out of fragment container (we need a fresh div each time) + div.parentNode.removeChild( div ); + } + } + + if ( elem.nodeType ) { + ret.push( elem ); + } else { + jQuery.merge( ret, elem ); + } + } + + // Fix #11356: Clear elements from safeFragment + if ( div ) { + elem = div = safe = null; + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + for ( i = 0; (elem = ret[i]) != null; i++ ) { + if ( jQuery.nodeName( elem, "input" ) ) { + fixDefaultChecked( elem ); + } else if ( typeof elem.getElementsByTagName !== "undefined" ) { + jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); + } + } + } + + // Append elements to a provided document fragment + if ( fragment ) { + // Special handling of each script element + handleScript = function( elem ) { + // Check if we consider it executable + if ( !elem.type || rscriptType.test( elem.type ) ) { + // Detach the script and store it in the scripts array (if provided) or the fragment + // Return truthy to indicate that it has been handled + return scripts ? + scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : + fragment.appendChild( elem ); + } + }; + + for ( i = 0; (elem = ret[i]) != null; i++ ) { + // Check if we're done after handling an executable script + if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { + // Append to fragment and handle embedded scripts + fragment.appendChild( elem ); + if ( typeof elem.getElementsByTagName !== "undefined" ) { + // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration + jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); + + // Splice the scripts into ret after their former ancestor and advance our index beyond them + ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); + i += jsTags.length; + } + } + } + } + + return ret; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var data, id, elem, type, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + jQuery.deletedIds.push( id ); + } + } + } + } + } +}); +// Limit scope pollution from any deprecated API +(function() { + +var matched, browser; + +// Use of jQuery.browser is frowned upon. +// More details: http://api.jquery.com/jQuery.browser +// jQuery.uaMatch maintained for back-compat +jQuery.uaMatch = function( ua ) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; +}; + +matched = jQuery.uaMatch( navigator.userAgent ); +browser = {}; + +if ( matched.browser ) { + browser[ matched.browser ] = true; + browser.version = matched.version; +} + +// Chrome is Webkit, but Webkit is also Safari. +if ( browser.chrome ) { + browser.webkit = true; +} else if ( browser.webkit ) { + browser.safari = true; +} + +jQuery.browser = browser; + +jQuery.sub = function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; +}; + +})(); +var curCSS, iframe, iframeDoc, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity=([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ), + elemdisplay = {}, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ], + + eventsToggle = jQuery.fn.toggle; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var elem, display, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + values[ index ] = jQuery._data( elem, "olddisplay" ); + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && elem.style.display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + display = curCSS( elem, "display" ); + + if ( !values[ index ] && display !== "none" ) { + jQuery._data( elem, "olddisplay", display ); + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state, fn2 ) { + var bool = typeof state === "boolean"; + + if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) { + return eventsToggle.apply( this, arguments ); + } + + return this.each(function() { + if ( bool ? state : isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + + } + } + } + }, + + // Exclude the following css properties to add px + cssNumber: { + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, numeric, extra ) { + var val, num, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( numeric || extra !== undefined ) { + num = parseFloat( val ); + return numeric || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +// NOTE: To any future maintainer, we've window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + curCSS = function( elem, name ) { + var ret, width, minWidth, maxWidth, + computed = window.getComputedStyle( elem, null ), + style = elem.style; + + if ( computed ) { + + ret = computed[ name ]; + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + curCSS = function( elem, name ) { + var left, rsLeft, + ret = elem.currentStyle && elem.currentStyle[ name ], + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + elem.runtimeStyle.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + elem.runtimeStyle.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + // we use jQuery.css instead of curCSS here + // because of the reliableMarginRight CSS hook! + val += jQuery.css( elem, extra + cssExpand[ i ], true ); + } + + // From this point on we use curCSS for maximum performance (relevant in animations) + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; + } + } else { + // at this point, extra isn't content, so add padding + val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + valueIsBorderBox = true, + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox + ) + ) + "px"; +} + + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + if ( elemdisplay[ nodeName ] ) { + return elemdisplay[ nodeName ]; + } + + var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ), + display = elem.css("display"); + elem.remove(); + + // If the simple way fails, + // get element's real default display by attaching it to a temp iframe + if ( display === "none" || display === "" ) { + // Use the already-created iframe if possible + iframe = document.body.appendChild( + iframe || jQuery.extend( document.createElement("iframe"), { + frameBorder: 0, + width: 0, + height: 0 + }) + ); + + // Create a cacheable copy of the iframe document on first call. + // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML + // document to it; WebKit & Firefox won't allow reusing the iframe document. + if ( !iframeDoc || !iframe.createElement ) { + iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; + iframeDoc.write(""); + iframeDoc.close(); + } + + elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) ); + + display = curCSS( elem, "display" ); + document.body.removeChild( iframe ); + } + + // Store the correct default display + elemdisplay[ nodeName ] = display; + + return display; +} + +jQuery.each([ "height", "width" ], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + // certain elements can have dimension info if we invisibly show them + // however, it must have a current display style that would benefit from this + if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) { + return jQuery.swap( elem, cssShow, function() { + return getWidthOrHeight( elem, name, extra ); + }); + } else { + return getWidthOrHeight( elem, name, extra ); + } + } + }, + + set: function( elem, value, extra ) { + return setPositiveNumber( elem, value, extra ? + augmentWidthOrHeight( + elem, + name, + extra, + jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box" + ) : 0 + ); + } + }; +}); + +if ( !jQuery.support.opacity ) { + jQuery.cssHooks.opacity = { + get: function( elem, computed ) { + // IE uses filters for opacity + return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? + ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : + computed ? "1" : ""; + }, + + set: function( elem, value ) { + var style = elem.style, + currentStyle = elem.currentStyle, + opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", + filter = currentStyle && currentStyle.filter || style.filter || ""; + + // IE has trouble with opacity if it does not have layout + // Force it by setting the zoom level + style.zoom = 1; + + // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 + if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && + style.removeAttribute ) { + + // Setting style.filter to null, "" & " " still leave "filter:" in the cssText + // if "filter:" is present at all, clearType is disabled, we want to avoid this + // style.removeAttribute is IE Only, but so apparently is this code path... + style.removeAttribute( "filter" ); + + // if there there is no filter style applied in a css rule, we are done + if ( currentStyle && !currentStyle.filter ) { + return; + } + } + + // otherwise, set new filter values + style.filter = ralpha.test( filter ) ? + filter.replace( ralpha, opacity ) : + filter + " " + opacity; + } + }; +} + +// These hooks cannot be added until DOM ready because the support test +// for it is not run until after DOM ready +jQuery(function() { + if ( !jQuery.support.reliableMarginRight ) { + jQuery.cssHooks.marginRight = { + get: function( elem, computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + return jQuery.swap( elem, { "display": "inline-block" }, function() { + if ( computed ) { + return curCSS( elem, "marginRight" ); + } + }); + } + }; + } + + // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 + // getComputedStyle returns percent when specified for top/left/bottom/right + // rather than make the css module depend on the offset module, we just check for it here + if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { + jQuery.each( [ "top", "left" ], function( i, prop ) { + jQuery.cssHooks[ prop ] = { + get: function( elem, computed ) { + if ( computed ) { + var ret = curCSS( elem, prop ); + // if curCSS returns percentage, fallback to offset + return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret; + } + } + }; + }); + } + +}); + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.hidden = function( elem ) { + return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none"); + }; + + jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); + }; +} + +// These hooks are used by animate to expand properties +jQuery.each({ + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i, + + // assumes a single number if not a string + parts = typeof value === "string" ? value.split(" ") : [ value ], + expanded = {}; + + for ( i = 0; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +}); +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, + rselectTextarea = /^(?:select|textarea)/i; + +jQuery.fn.extend({ + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map(function(){ + return this.elements ? jQuery.makeArray( this.elements ) : this; + }) + .filter(function(){ + return this.name && !this.disabled && + ( this.checked || rselectTextarea.test( this.nodeName ) || + rinput.test( this.type ) ); + }) + .map(function( i, elem ){ + var val = jQuery( this ).val(); + + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val, i ){ + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); + +//Serialize an array of form elements or a set of +//key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; + + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); + + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); +}; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // If array item is non-scalar (array or object), encode its + // numeric index to resolve deserialization ambiguity issues. + // Note that rack (as of 1.0.0) can't currently deserialize + // nested arrays properly, and attempting to do so may cause + // a server error. Possible fixes are to modify rack's + // deserialization algorithm or to provide an option or flag + // to force array serialization to be shallow. + buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && jQuery.type( obj ) === "object" ) { + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + // Serialize scalar item. + add( prefix, obj ); + } +} +var + // Document location + ajaxLocParts, + ajaxLocation, + + rhash = /#.*$/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + rquery = /\?/, + rscript = /)<[^<]*)*<\/script>/gi, + rts = /([?&])_=[^&]*/, + rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, + + // Keep a copy of the old load method + _load = jQuery.fn.load, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = ["*/"] + ["*"]; + +// #8138, IE may throw an exception when accessing +// a field from window.location if document.domain has been set +try { + ajaxLocation = location.href; +} catch( e ) { + // Use the href attribute of an A element + // since IE will modify it given document.location + ajaxLocation = document.createElement( "a" ); + ajaxLocation.href = ""; + ajaxLocation = ajaxLocation.href; +} + +// Segment location into parts +ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, list, placeBefore, + dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ), + i = 0, + length = dataTypes.length; + + if ( jQuery.isFunction( func ) ) { + // For each dataType in the dataTypeExpression + for ( ; i < length; i++ ) { + dataType = dataTypes[ i ]; + // We control if we're asked to add before + // any existing element + placeBefore = /^\+/.test( dataType ); + if ( placeBefore ) { + dataType = dataType.substr( 1 ) || "*"; + } + list = structure[ dataType ] = structure[ dataType ] || []; + // then we add to the structure accordingly + list[ placeBefore ? "unshift" : "push" ]( func ); + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, + dataType /* internal */, inspected /* internal */ ) { + + dataType = dataType || options.dataTypes[ 0 ]; + inspected = inspected || {}; + + inspected[ dataType ] = true; + + var selection, + list = structure[ dataType ], + i = 0, + length = list ? list.length : 0, + executeOnly = ( structure === prefilters ); + + for ( ; i < length && ( executeOnly || !selection ); i++ ) { + selection = list[ i ]( options, originalOptions, jqXHR ); + // If we got redirected to another dataType + // we try there if executing only and not done already + if ( typeof selection === "string" ) { + if ( !executeOnly || inspected[ selection ] ) { + selection = undefined; + } else { + options.dataTypes.unshift( selection ); + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, selection, inspected ); + } + } + } + // If we're only executing or nothing was selected + // we try the catchall dataType if not done already + if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { + selection = inspectPrefiltersOrTransports( + structure, options, originalOptions, jqXHR, "*", inspected ); + } + // unnecessary when only executing (prefilters) + // but it'll be ignored by the caller in that case + return selection; +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } +} + +jQuery.fn.load = function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); + } + + // Don't do a request if no elements are being requested + if ( !this.length ) { + return this; + } + + var selector, type, response, + self = this, + off = url.indexOf(" "); + + if ( off >= 0 ) { + selector = url.slice( off, url.length ); + url = url.slice( 0, off ); + } + + // If it's a function + if ( jQuery.isFunction( params ) ) { + + // We assume that it's the callback + callback = params; + params = undefined; + + // Otherwise, build a param string + } else if ( params && typeof params === "object" ) { + type = "POST"; + } + + // Request the remote document + jQuery.ajax({ + url: url, + + // if "type" variable is undefined, then "GET" method will be used + type: type, + dataType: "html", + data: params, + complete: function( jqXHR, status ) { + if ( callback ) { + self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); + } + } + }).done(function( responseText ) { + + // Save response for use in complete callback + response = arguments; + + // See if a selector was specified + self.html( selector ? + + // Create a dummy div to hold the results + jQuery("
") + + // inject the contents of the document in, removing the scripts + // to avoid any 'Permission Denied' errors in IE + .append( responseText.replace( rscript, "" ) ) + + // Locate the specified elements + .find( selector ) : + + // If not, just inject the full result + responseText ); + + }); + + return this; +}; + +// Attach a bunch of functions for handling common AJAX events +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ + jQuery.fn[ o ] = function( f ){ + return this.on( o, f ); + }; +}); + +jQuery.each( [ "get", "post" ], function( i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + // shift arguments if data argument was omitted + if ( jQuery.isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + type: method, + url: url, + data: data, + success: callback, + dataType: type + }); + }; +}); + +jQuery.extend({ + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + if ( settings ) { + // Building a settings object + ajaxExtend( target, jQuery.ajaxSettings ); + } else { + // Extending ajaxSettings + settings = target; + target = jQuery.ajaxSettings; + } + ajaxExtend( target, settings ); + return target; + }, + + ajaxSettings: { + url: ajaxLocation, + isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), + global: true, + type: "GET", + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + processData: true, + async: true, + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + xml: "application/xml, text/xml", + html: "text/html", + text: "text/plain", + json: "application/json, text/javascript", + "*": allTypes + }, + + contents: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText" + }, + + // List of data converters + // 1) key format is "source_type destination_type" (a single space in-between) + // 2) the catchall symbol "*" can be used for source_type + converters: { + + // Convert anything to text + "* text": window.String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": jQuery.parseJSON, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + context: true, + url: true + } + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var // ifModified key + ifModifiedKey, + // Response headers + responseHeadersString, + responseHeaders, + // transport + transport, + // timeout handle + timeoutTimer, + // Cross-domain detection vars + parts, + // To know if global events are to be dispatched + fireGlobals, + // Loop variable + i, + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + // Callbacks context + callbackContext = s.context || s, + // Context for global events + // It's the callbackContext if one was provided in the options + // and if it's a DOM node or a jQuery collection + globalEventContext = callbackContext !== s && + ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? + jQuery( callbackContext ) : jQuery.event, + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + // Status-dependent callbacks + statusCode = s.statusCode || {}, + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + // The jqXHR state + state = 0, + // Default abort message + strAbort = "canceled", + // Fake xhr + jqXHR = { + + readyState: 0, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( !state ) { + var lname = name.toLowerCase(); + name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Raw string + getAllResponseHeaders: function() { + return state === 2 ? responseHeadersString : null; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( state === 2 ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; + } + } + match = responseHeaders[ key.toLowerCase() ]; + } + return match === undefined ? null : match; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( !state ) { + s.mimeType = type; + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + statusText = statusText || strAbort; + if ( transport ) { + transport.abort( statusText ); + } + done( 0, statusText ); + return this; + } + }; + + // Callback for when everything is done + // It is defined here because jslint complains if it is declared + // at the end of the function (which would be more logical and readable) + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Called once + if ( state === 2 ) { + return; + } + + // State is "done" now + state = 2; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // If successful, handle type chaining + if ( status >= 200 && status < 300 || status === 304 ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + + modified = jqXHR.getResponseHeader("Last-Modified"); + if ( modified ) { + jQuery.lastModified[ ifModifiedKey ] = modified; + } + modified = jqXHR.getResponseHeader("Etag"); + if ( modified ) { + jQuery.etag[ ifModifiedKey ] = modified; + } + } + + // If not modified + if ( status === 304 ) { + + statusText = "notmodified"; + isSuccess = true; + + // If we have data + } else { + + isSuccess = ajaxConvert( s, response ); + statusText = isSuccess.state; + success = isSuccess.data; + error = isSuccess.error; + isSuccess = !error; + } + } else { + // We extract error from statusText + // then normalize statusText and status for non-aborts + error = statusText; + if ( !statusText || status ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + // Attach deferreds + deferred.promise( jqXHR ); + jqXHR.success = jqXHR.done; + jqXHR.error = jqXHR.fail; + jqXHR.complete = completeDeferred.add; + + // Status-dependent callbacks + jqXHR.statusCode = function( map ) { + if ( map ) { + var tmp; + if ( state < 2 ) { + for ( tmp in map ) { + statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; + } + } else { + tmp = map[ jqXHR.status ]; + jqXHR.always( tmp ); + } + } + return this; + }; + + // Remove hash character (#7531: and string promotion) + // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // We also use the url parameter if available + s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + + // Extract dataTypes list + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace ); + + // A cross-domain request is in order when we have a protocol:host:port mismatch + if ( s.crossDomain == null ) { + parts = rurl.exec( s.url.toLowerCase() ) || false; + s.crossDomain = parts && ( parts.join(":") + ( parts[ 3 ] ? "" : parts[ 1 ] === "http:" ? 80 : 443 ) ) !== + ( ajaxLocParts.join(":") + ( ajaxLocParts[ 3 ] ? "" : ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ); + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( state === 2 ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + fireGlobals = s.global; + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // If data is available, append data to url + if ( s.data ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Get ifModifiedKey before adding the anti-cache parameter + ifModifiedKey = s.url; + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = s.url.replace( rts, "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + ifModifiedKey = ifModifiedKey || s.url; + if ( jQuery.lastModified[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); + } + if ( jQuery.etag[ ifModifiedKey ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); + } + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? + s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { + // Abort if not done already and return + return jqXHR.abort(); + + } + + // aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + for ( i in { success: 1, error: 1, complete: 1 } ) { + jqXHR[ i ]( s[ i ] ); + } + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout( function(){ + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + state = 1; + transport.send( requestHeaders, done ); + } catch (e) { + // Propagate exception as error if not done + if ( state < 2 ) { + done( -1, e ); + // Simply rethrow otherwise + } else { + throw e; + } + } + } + + return jqXHR; + }, + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {} + +}); + +/* Handles responses to an ajax request: + * - sets all responseXXX fields accordingly + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes, + responseFields = s.responseFields; + + // Fill responseXXX fields + for ( type in responseFields ) { + if ( type in responses ) { + jqXHR[ responseFields[type] ] = responses[ type ]; + } + } + + // Remove auto dataType and get content-type in the process + while( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +// Chain conversions given the request and the original response +function ajaxConvert( s, response ) { + + var conv, conv2, current, tmp, + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(), + prev = dataTypes[ 0 ], + converters = {}, + i = 0; + + // Apply the dataFilter if provided + if ( s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + // Convert to each sequential dataType, tolerating list modification + for ( ; (current = dataTypes[++i]); ) { + + // There's only work to do if current dataType is non-auto + if ( current !== "*" ) { + + // Convert response if prev dataType is non-auto and differs from current + if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split(" "); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.splice( i--, 0, current ); + } + + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s["throws"] ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; + } + } + } + } + + // Update prev for next iteration + prev = current; + } + } + + return { state: "success", data: response }; +} +var oldCallbacks = [], + rquestion = /\?/, + rjsonp = /(=)\?(?=&|$)|\?\?/, + nonce = jQuery.now(); + +// Default jsonp settings +jQuery.ajaxSetup({ + jsonp: "callback", + jsonpCallback: function() { + var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); + this[ callback ] = true; + return callback; + } +}); + +// Detect, normalize options and install callbacks for jsonp requests +jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { + + var callbackName, overwritten, responseContainer, + data = s.data, + url = s.url, + hasCallback = s.jsonp !== false, + replaceInUrl = hasCallback && rjsonp.test( url ), + replaceInData = hasCallback && !replaceInUrl && typeof data === "string" && + !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && + rjsonp.test( data ); + + // Handle iff the expected data type is "jsonp" or we have a parameter to set + if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) { + + // Get callback name, remembering preexisting value associated with it + callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? + s.jsonpCallback() : + s.jsonpCallback; + overwritten = window[ callbackName ]; + + // Insert callback into url or form data + if ( replaceInUrl ) { + s.url = url.replace( rjsonp, "$1" + callbackName ); + } else if ( replaceInData ) { + s.data = data.replace( rjsonp, "$1" + callbackName ); + } else if ( hasCallback ) { + s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; + } + + // Use data converter to retrieve json after script execution + s.converters["script json"] = function() { + if ( !responseContainer ) { + jQuery.error( callbackName + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // force json dataType + s.dataTypes[ 0 ] = "json"; + + // Install callback + window[ callbackName ] = function() { + responseContainer = arguments; + }; + + // Clean-up function (fires after converters) + jqXHR.always(function() { + // Restore preexisting value + window[ callbackName ] = overwritten; + + // Save back as free + if ( s[ callbackName ] ) { + // make sure that re-using the options doesn't screw things around + s.jsonpCallback = originalSettings.jsonpCallback; + + // save the callback name for future use + oldCallbacks.push( callbackName ); + } + + // Call if it was a function and we have a response + if ( responseContainer && jQuery.isFunction( overwritten ) ) { + overwritten( responseContainer[ 0 ] ); + } + + responseContainer = overwritten = undefined; + }); + + // Delegate to script + return "script"; + } +}); +// Install script dataType +jQuery.ajaxSetup({ + accepts: { + script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /javascript|ecmascript/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +}); + +// Handle cache's special case and global +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + s.global = false; + } +}); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function(s) { + + // This transport only deals with cross domain requests + if ( s.crossDomain ) { + + var script, + head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; + + return { + + send: function( _, callback ) { + + script = document.createElement( "script" ); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function( _, isAbort ) { + + if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + // Dereference the script + script = undefined; + + // Callback if not abort + if ( !isAbort ) { + callback( 200, "success" ); + } + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function() { + if ( script ) { + script.onload( 0, 1 ); + } + } + }; + } +}); +var xhrCallbacks, + // #5280: Internet Explorer will keep connections alive if we don't abort on unload + xhrOnUnloadAbort = window.ActiveXObject ? function() { + // Abort all pending requests + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ]( 0, 1 ); + } + } : false, + xhrId = 0; + +// Functions to create xhrs +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch( e ) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject( "Microsoft.XMLHTTP" ); + } catch( e ) {} +} + +// Create the request object +// (This is still attached to ajaxSettings for backward compatibility) +jQuery.ajaxSettings.xhr = window.ActiveXObject ? + /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function() { + return !this.isLocal && createStandardXHR() || createActiveXHR(); + } : + // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +// Determine support properties +(function( xhr ) { + jQuery.extend( jQuery.support, { + ajax: !!xhr, + cors: !!xhr && ( "withCredentials" in xhr ) + }); +})( jQuery.ajaxSettings.xhr() ); + +// Create transport if the browser can provide an xhr +if ( jQuery.support.ajax ) { + + jQuery.ajaxTransport(function( s ) { + // Cross domain only allowed if supported through XMLHttpRequest + if ( !s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + send: function( headers, complete ) { + + // Get a new xhr + var handle, i, + xhr = s.xhr(); + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open( s.type, s.url, s.async, s.username, s.password ); + } else { + xhr.open( s.type, s.url, s.async ); + } + + // Apply custom fields if provided + if ( s.xhrFields ) { + for ( i in s.xhrFields ) { + xhr[ i ] = s.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( s.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( s.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !s.crossDomain && !headers["X-Requested-With"] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + } catch( _ ) {} + + // Do send the request + // This may raise an exception which is actually + // handled in jQuery.ajax (so no try/catch here) + xhr.send( ( s.hasContent && s.data ) || null ); + + // Listener + callback = function( _, isAbort ) { + + var status, + statusText, + responseHeaders, + responses, + xml; + + // Firefox throws exceptions when accessing properties + // of an xhr when a network error occurred + // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) + try { + + // Was never called and is aborted or complete + if ( callback && ( isAbort || xhr.readyState === 4 ) ) { + + // Only called once + callback = undefined; + + // Do not keep as active anymore + if ( handle ) { + xhr.onreadystatechange = jQuery.noop; + if ( xhrOnUnloadAbort ) { + delete xhrCallbacks[ handle ]; + } + } + + // If it's an abort + if ( isAbort ) { + // Abort it manually if needed + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + } else { + status = xhr.status; + responseHeaders = xhr.getAllResponseHeaders(); + responses = {}; + xml = xhr.responseXML; + + // Construct response list + if ( xml && xml.documentElement /* #4958 */ ) { + responses.xml = xml; + } + + // When requesting binary data, IE6-9 will throw an exception + // on any attempt to access responseText (#11426) + try { + responses.text = xhr.responseText; + } catch( _ ) { + } + + // Firefox throws an exception when accessing + // statusText for faulty cross-domain requests + try { + statusText = xhr.statusText; + } catch( e ) { + // We normalize with Webkit giving an empty statusText + statusText = ""; + } + + // Filter status for non standard behaviors + + // If the request is local and we have data: assume a success + // (success with no data won't get notified, that's the best we + // can do given current implementations) + if ( !status && s.isLocal && !s.crossDomain ) { + status = responses.text ? 200 : 404; + // IE - #1450: sometimes returns 1223 when it should be 204 + } else if ( status === 1223 ) { + status = 204; + } + } + } + } catch( firefoxAccessException ) { + if ( !isAbort ) { + complete( -1, firefoxAccessException ); + } + } + + // Call complete if needed + if ( responses ) { + complete( status, statusText, responses, responseHeaders ); + } + }; + + if ( !s.async ) { + // if we're in sync mode we fire the callback + callback(); + } else if ( xhr.readyState === 4 ) { + // (IE6 & IE7) if it's in cache and has been + // retrieved directly we need to fire the callback + setTimeout( callback, 0 ); + } else { + handle = ++xhrId; + if ( xhrOnUnloadAbort ) { + // Create the active xhrs callbacks list if needed + // and attach the unload handler + if ( !xhrCallbacks ) { + xhrCallbacks = {}; + jQuery( window ).unload( xhrOnUnloadAbort ); + } + // Add to list of active xhrs callbacks + xhrCallbacks[ handle ] = callback; + } + xhr.onreadystatechange = callback; + } + }, + + abort: function() { + if ( callback ) { + callback(0,1); + } + } + }; + } + }); +} +var fxNow, timerId, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ), + rrun = /queueHooks$/, + animationPrefilters = [ defaultPrefilter ], + tweeners = { + "*": [function( prop, value ) { + var end, unit, + tween = this.createTween( prop, value ), + parts = rfxnum.exec( value ), + target = tween.cur(), + start = +target || 0, + scale = 1, + maxIterations = 20; + + if ( parts ) { + end = +parts[2]; + unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + + // We need to compute starting value + if ( unit !== "px" && start ) { + // Iteratively approximate from a nonzero starting point + // Prefer the current property, because this process will be trivial if it uses the same units + // Fallback to end or a simple constant + start = jQuery.css( tween.elem, prop, true ) || end || 1; + + do { + // If previous iteration zeroed out, double until we get *something* + // Use a string for doubling factor so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + start = start / scale; + jQuery.style( tween.elem, prop, start + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // And breaking the loop if scale is unchanged or perfect, or if we've just had enough + } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); + } + + tween.unit = unit; + tween.start = start; + // If a +=/-= token was provided, we're doing a relative animation + tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end; + } + return tween; + }] + }; + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout(function() { + fxNow = undefined; + }, 0 ); + return ( fxNow = jQuery.now() ); +} + +function createTweens( animation, props ) { + jQuery.each( props, function( prop, value ) { + var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( collection[ index ].call( animation, prop, value ) ) { + + // we're done with this property + return; + } + } + }); +} + +function Animation( elem, properties, options ) { + var result, + index = 0, + tweenerIndex = 0, + length = animationPrefilters.length, + deferred = jQuery.Deferred().always( function() { + // don't match elem in the :animated selector + delete tick.elem; + }), + tick = function() { + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + percent = 1 - ( remaining / animation.duration || 0 ), + index = 0, + length = animation.tweens.length; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ]); + + if ( percent < 1 && length ) { + return remaining; + } else { + deferred.resolveWith( elem, [ animation ] ); + return false; + } + }, + animation = deferred.promise({ + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { specialEasing: {} }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end, easing ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + // if we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // resolve when we played the last frame + // otherwise, reject + if ( gotoEnd ) { + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + }), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length ; index++ ) { + result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + return result; + } + } + + createTweens( animation, props ); + + if ( jQuery.isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + jQuery.fx.timer( + jQuery.extend( tick, { + anim: animation, + queue: animation.opts.queue, + elem: elem + }) + ); + + // attach callbacks from options + return animation.progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( jQuery.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // not quite $.extend, this wont overwrite keys already present. + // also - reusing 'index' from above because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.split(" "); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length ; index++ ) { + prop = props[ index ]; + tweeners[ prop ] = tweeners[ prop ] || []; + tweeners[ prop ].unshift( callback ); + } + }, + + prefilter: function( callback, prepend ) { + if ( prepend ) { + animationPrefilters.unshift( callback ); + } else { + animationPrefilters.push( callback ); + } + } +}); + +function defaultPrefilter( elem, props, opts ) { + var index, prop, value, length, dataShow, tween, hooks, oldfire, + anim = this, + style = elem.style, + orig = {}, + handled = [], + hidden = elem.nodeType && isHidden( elem ); + + // handle queue: false promises + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always(function() { + // doing this makes sure that the complete handler will be called + // before this completes + anim.always(function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + }); + }); + } + + // height/width overflow pass + if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE does not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + if ( jQuery.css( elem, "display" ) === "inline" && + jQuery.css( elem, "float" ) === "none" ) { + + // inline-level elements accept inline-block; + // block-level elements need to be inline with layout + if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { + style.display = "inline-block"; + + } else { + style.zoom = 1; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + if ( !jQuery.support.shrinkWrapBlocks ) { + anim.done(function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + }); + } + } + + + // show/hide pass + for ( index in props ) { + value = props[ index ]; + if ( rfxtypes.exec( value ) ) { + delete props[ index ]; + if ( value === ( hidden ? "hide" : "show" ) ) { + continue; + } + handled.push( index ); + } + } + + length = handled.length; + if ( length ) { + dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); + if ( hidden ) { + jQuery( elem ).show(); + } else { + anim.done(function() { + jQuery( elem ).hide(); + }); + } + anim.done(function() { + var prop; + jQuery.removeData( elem, "fxshow", true ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + }); + for ( index = 0 ; index < length ; index++ ) { + prop = handled[ index ]; + tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); + orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); + + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = tween.start; + if ( hidden ) { + tween.end = tween.start; + tween.start = prop === "width" || prop === "height" ? 1 : 0; + } + } + } + } +} + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || "swing"; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem[ tween.prop ] != null && + (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { + return tween.elem[ tween.prop ]; + } + + // passing any value as a 4th parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails + // so, simple values such as "10px" are parsed to Float. + // complex values such as "rotate(1rad)" are returned as is. + result = jQuery.css( tween.elem, tween.prop, false, "" ); + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + // use step hook for back compat - use cssHook if its there - use .style if its + // available and use plain properties where available + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Remove in 2.0 - this supports IE8's panic based approach +// to setting things on disconnected nodes + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" || + // special check for .toggle( handler, handler, ... ) + ( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +}); + +jQuery.fn.extend({ + fadeTo: function( speed, to, easing, callback ) { + + // show any hidden elements after setting opacity to 0 + return this.filter( isHidden ).css( "opacity", 0 ).show() + + // animate to the value specified + .end().animate({ opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations resolve immediately + if ( empty ) { + anim.stop( true ); + } + }; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = jQuery._data( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + }); + } +}); + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + attrs = { height: type }, + i = 0; + + // if we include width, step value is 1 to do all cssExpand values, + // if we don't include width, step value is 2 to skip over Left and Right + includeWidth = includeWidth? 1 : 0; + for( ; i < 4 ; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx("show"), + slideUp: genFx("hide"), + slideToggle: genFx("toggle"), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +}); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; + + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p*Math.PI ) / 2; + } +}; + +jQuery.timers = []; +jQuery.fx = Tween.prototype.init; +jQuery.fx.tick = function() { + var timer, + timers = jQuery.timers, + i = 0; + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } +}; + +jQuery.fx.timer = function( timer ) { + if ( timer() && jQuery.timers.push( timer ) && !timerId ) { + timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); + } +}; + +jQuery.fx.interval = 13; + +jQuery.fx.stop = function() { + clearInterval( timerId ); + timerId = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + // Default speed + _default: 400 +}; + +// Back Compat <1.8 extension point +jQuery.fx.step = {}; + +if ( jQuery.expr && jQuery.expr.filters ) { + jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; + }; +} +var rroot = /^(?:body|html)$/i; + +jQuery.fn.offset = function( options ) { + if ( arguments.length ) { + return options === undefined ? + this : + this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft, + box = { top: 0, left: 0 }, + elem = this[ 0 ], + doc = elem && elem.ownerDocument; + + if ( !doc ) { + return; + } + + if ( (body = doc.body) === elem ) { + return jQuery.offset.bodyOffset( elem ); + } + + docElem = doc.documentElement; + + // Make sure it's not a disconnected DOM node + if ( !jQuery.contains( docElem, elem ) ) { + return box; + } + + // If we don't have gBCR, just use 0,0 rather than error + // BlackBerry 5, iOS 3 (original iPhone) + if ( typeof elem.getBoundingClientRect !== "undefined" ) { + box = elem.getBoundingClientRect(); + } + win = getWindow( doc ); + clientTop = docElem.clientTop || body.clientTop || 0; + clientLeft = docElem.clientLeft || body.clientLeft || 0; + scrollTop = win.pageYOffset || docElem.scrollTop; + scrollLeft = win.pageXOffset || docElem.scrollLeft; + return { + top: box.top + scrollTop - clientTop, + left: box.left + scrollLeft - clientLeft + }; +}; + +jQuery.offset = { + + bodyOffset: function( body ) { + var top = body.offsetTop, + left = body.offsetLeft; + + if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { + top += parseFloat( jQuery.css(body, "marginTop") ) || 0; + left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; + } + + return { top: top, left: left }; + }, + + setOffset: function( elem, options, i ) { + var position = jQuery.css( elem, "position" ); + + // set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + var curElem = jQuery( elem ), + curOffset = curElem.offset(), + curCSSTop = jQuery.css( elem, "top" ), + curCSSLeft = jQuery.css( elem, "left" ), + calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, + props = {}, curPosition = {}, curTop, curLeft; + + // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( jQuery.isFunction( options ) ) { + options = options.call( elem, i, curOffset ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + } else { + curElem.css( props ); + } + } +}; + + +jQuery.fn.extend({ + + position: function() { + if ( !this[0] ) { + return; + } + + var elem = this[0], + + // Get *real* offsetParent + offsetParent = this.offsetParent(), + + // Get correct offsets + offset = this.offset(), + parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); + + // Subtract element margins + // note: when an element has margin: auto the offsetLeft and marginLeft + // are the same in Safari causing offset.left to incorrectly be 0 + offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; + offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; + + // Add offsetParent borders + parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; + parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; + + // Subtract the two offsets + return { + top: offset.top - parentOffset.top, + left: offset.left - parentOffset.left + }; + }, + + offsetParent: function() { + return this.map(function() { + var offsetParent = this.offsetParent || document.body; + while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || document.body; + }); + } +}); + + +// Create scrollLeft and scrollTop methods +jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) { + var top = /Y/.test( prop ); + + jQuery.fn[ method ] = function( val ) { + return jQuery.access( this, function( elem, method, val ) { + var win = getWindow( elem ); + + if ( val === undefined ) { + return win ? (prop in win) ? win[ prop ] : + win.document.documentElement[ method ] : + elem[ method ]; + } + + if ( win ) { + win.scrollTo( + !top ? val : jQuery( win ).scrollLeft(), + top ? val : jQuery( win ).scrollTop() + ); + + } else { + elem[ method ] = val; + } + }, method, val, arguments.length, null ); + }; +}); + +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? + elem : + elem.nodeType === 9 ? + elem.defaultView || elem.parentWindow : + false; +} +// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods +jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { + jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { + // margin is only for outerHeight, outerWidth + jQuery.fn[ funcName ] = function( margin, value ) { + var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), + extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); + + return jQuery.access( this, function( elem, type, value ) { + var doc; + + if ( jQuery.isWindow( elem ) ) { + // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there + // isn't a whole lot we can do. See pull request at this URL for discussion: + // https://github.com/jquery/jquery/pull/764 + return elem.document.documentElement[ "client" + name ]; + } + + // Get document width or height + if ( elem.nodeType === 9 ) { + doc = elem.documentElement; + + // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest + // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. + return Math.max( + elem.body[ "scroll" + name ], doc[ "scroll" + name ], + elem.body[ "offset" + name ], doc[ "offset" + name ], + doc[ "client" + name ] + ); + } + + return value === undefined ? + // Get width or height on the element, requesting but not forcing parseFloat + jQuery.css( elem, type, value, extra ) : + + // Set width or height on the element + jQuery.style( elem, type, value, extra ); + }, type, chainable ? margin : undefined, chainable, null ); + }; + }); +}); +// Expose jQuery to the global object +window.jQuery = window.$ = jQuery; + +// Expose jQuery as an AMD module, but only for AMD loaders that +// understand the issues with loading multiple versions of jQuery +// in a page that all might call define(). The loader will indicate +// they have special allowances for multiple jQuery versions by +// specifying define.amd.jQuery = true. Register as a named module, +// since jQuery can be concatenated with other files that may use define, +// but not use a proper concatenation script that understands anonymous +// AMD modules. A named AMD is safest and most robust way to register. +// Lowercase jquery is used because AMD module names are derived from +// file names, and jQuery is normally delivered in a lowercase file name. +// Do this after creating the global so that if an AMD module wants to call +// noConflict to hide this version of jQuery, it will work. +if ( typeof define === "function" && define.amd && define.amd.jQuery ) { + define( "jquery", [], function () { return jQuery; } ); +} + +})( window ); diff --git a/test/www/regression/pjs-13551/child1.html b/test/www/regression/pjs-13551/child1.html new file mode 100644 index 0000000..1f9ee65 --- /dev/null +++ b/test/www/regression/pjs-13551/child1.html @@ -0,0 +1,2 @@ + + diff --git a/test/www/regression/pjs-13551/child1a.html b/test/www/regression/pjs-13551/child1a.html new file mode 100644 index 0000000..fdaa7f1 --- /dev/null +++ b/test/www/regression/pjs-13551/child1a.html @@ -0,0 +1,12 @@ + + + + diff --git a/test/www/regression/pjs-13551/child2.html b/test/www/regression/pjs-13551/child2.html new file mode 100644 index 0000000..93322fc --- /dev/null +++ b/test/www/regression/pjs-13551/child2.html @@ -0,0 +1,9 @@ + + + + +

success

diff --git a/test/www/regression/pjs-13551/closing-parent.html b/test/www/regression/pjs-13551/closing-parent.html new file mode 100644 index 0000000..5ad4d2b --- /dev/null +++ b/test/www/regression/pjs-13551/closing-parent.html @@ -0,0 +1,23 @@ + + + + diff --git a/test/www/regression/pjs-13551/reloading-parent.html b/test/www/regression/pjs-13551/reloading-parent.html new file mode 100644 index 0000000..6364ec6 --- /dev/null +++ b/test/www/regression/pjs-13551/reloading-parent.html @@ -0,0 +1,22 @@ + + + + diff --git a/test/www/regression/webkit-60448.html b/test/www/regression/webkit-60448.html new file mode 100644 index 0000000..1de358b --- /dev/null +++ b/test/www/regression/webkit-60448.html @@ -0,0 +1,15 @@ + + +Test passes if it does not crash. + +
+ + + \ No newline at end of file diff --git a/test/www/render/image.jpg b/test/www/render/image.jpg new file mode 100644 index 0000000..53de7f6 Binary files /dev/null and b/test/www/render/image.jpg differ diff --git a/test/www/render/index.html b/test/www/render/index.html new file mode 100644 index 0000000..aa7e80f --- /dev/null +++ b/test/www/render/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/test/www/status.py b/test/www/status.py new file mode 100644 index 0000000..e7a1c3e --- /dev/null +++ b/test/www/status.py @@ -0,0 +1,50 @@ +import cStringIO as StringIO +import urlparse + +def html_esc(s): + return s.replace('&','&').replace('<','<').replace('>','>') + +def handle_request(req): + url = urlparse.urlparse(req.path) + headers = [] + body = "" + + try: + query = urlparse.parse_qsl(url.query, strict_parsing=True) + status = None + for key, value in query: + if key == 'status': + if status is not None: + raise ValueError("status can only be specified once") + status = int(value) + elif key == 'Content-Type' or key == 'Content-Length': + raise ValueError("cannot override " + key) + else: + headers.append((key, value)) + + if status is None: + status = 200 + + body = "

Status: {}

".format(status) + if headers: + body += "
"
+            for key, value in headers:
+                body += html_esc("{}: {}\n".format(key, value))
+            body += "
" + + except Exception as e: + try: + status = int(url.query) + body = "

Status: {}

".format(status) + except: + status = 400 + body = "

Status: 400

" + body += "
" + html_esc(str(e)) + "
" + + req.send_response(status) + req.send_header('Content-Type', 'text/html') + req.send_header('Content-Length', str(len(body))) + for key, value in headers: + req.send_header(key, value) + req.end_headers() + return StringIO.StringIO(body) diff --git a/test/www/url-encoding.py b/test/www/url-encoding.py new file mode 100644 index 0000000..300dfdc --- /dev/null +++ b/test/www/url-encoding.py @@ -0,0 +1,85 @@ +# -*- encoding: utf-8 -*- +import urlparse +from cStringIO import StringIO +import time + +def html_esc(s): + return s.replace('&','&').replace('<','<').replace('>','>') + +def do_response(req, body, code=200, headers={}): + req.send_response(code) + req.send_header('Content-Length', str(len(body))) + if 'Content-Type' not in headers: + req.send_header('Content-Type', 'text/html') + for k, v in headers.items(): + if k != 'Content-Length': + req.send_header(k, v) + req.end_headers() + return StringIO(body) + +def do_redirect(req, target): + return do_response(req, + 'Go here'.format(target), + code=302, headers={ 'Location': target }) + +def handle_request(req): + url = urlparse.urlparse(req.path) + + # This handler returns one of several different documents, + # depending on the query string. Many of the URLs involved contain + # text encoded in Shift_JIS, and will not round-trip correctly if + # misinterpreted as UTF-8. Comments indicate the Unicode equivalent. + + if url.query == '/': + return do_redirect(req, '?/%83y%81[%83W') + + elif url.query == '/f': + return do_response(req, + '' + 'framed' + '' + '' + '' + '') + + elif url.query == "/r": + return do_response(req, + '') + + elif url.query == "/re": + return do_response(req, + '' + '' + '') + + elif url.query == "/%83y%81[%83W": # ページ + return do_response(req, '

PASS

') + + elif url.query == "/%98g": # 枠 + return do_response(req, '

PASS

') + + elif url.query == "/%95s%96%D1%82%C8%98_%91%88": # 不毛な論争 + return do_response(req, '

FRAME

') + + elif url.query == "/%8F%91": # 書 + return do_response(req, + 'window.onload=function(){' + 'document.body.innerHTML="

PASS

";};', + headers={'Content-Type': 'application/javascript'}) + + elif url.query == "/%8C%CC%8F%E1": # 故障 + return do_response(req, + 'internal server error', + code=500) + + elif url.query == "/%89i%8Bv": # 永久 + time.sleep(5) + return do_response(req, '', code=204) + + else: + return do_response(req, + '404 Not Found' + '

URL not found: {}

' + .format(html_esc(req.path)), + code=404) diff --git a/test/www/user-agent.html b/test/www/user-agent.html new file mode 100644 index 0000000..adab8e8 --- /dev/null +++ b/test/www/user-agent.html @@ -0,0 +1,9 @@ + + +User Agent + + +

User agent is: Unknown.

+ + + diff --git a/third-party.txt b/third-party.txt new file mode 100644 index 0000000..48f74c4 --- /dev/null +++ b/third-party.txt @@ -0,0 +1,36 @@ +This document contains the list of Third Party Software included with +PhantomJS, along with the license information. + +Third Party Software may impose additional restrictions and it is the +user's responsibility to ensure that they have met the licensing +requirements of PhantomJS and the relevant license of the Third Party +Software they are using. + +Qt - http://qt-project.org/ +License: GNU Lesser General Public License (LGPL) version 2.1. +Reference: http://qt-project.org/doc/qt-4.8/lgpl.html. + +WebKit - http://www.webkit.org/ +License: GNU Lesser General Public License (LGPL) version 2.1 and BSD. +Reference: http://www.webkit.org/coding/lgpl-license.html and +http://www.webkit.org/coding/bsd-license.html. + +Mongoose - https://github.com/cesanta/mongoose +License: MIT +Reference: https://github.com/cesanta/mongoose/commit/abbf27338ef554cce0281ac157aa71a9c1b82a55 + +OpenSSL - http://www.openssl.org/ +License: OpenSSL License, SSLeay License. +Reference: http://www.openssl.org/source/license.html. + +Linenoise - https://github.com/tadmarshall/linenoise +License: BSD. +Reference: https://github.com/tadmarshall/linenoise/blob/master/linenoise.h. + +QCommandLine - http://xf.iksaif.net/dev/qcommandline.html +License: GNU Lesser General Public License (LGPL) version 2.1. +Reference: http://dev.iksaif.net/projects/qcommandline/repository/revisions/master/entry/COPYING + +wkhtmlpdf - http://code.google.com/p/wkhtmltopdf/ +License: GNU Lesser General Public License (LGPL) +Reference: http://code.google.com/p/wkhtmltopdf/ diff --git a/tools/check-style.bat b/tools/check-style.bat new file mode 100644 index 0000000..e412b30 --- /dev/null +++ b/tools/check-style.bat @@ -0,0 +1,20 @@ +@echo off + +set ROOT_DIR=%CD% + +set CONFIG= +set CONFIG=%CONFIG% --indent=spaces=4 +set CONFIG=%CONFIG% --style=otbs +set CONFIG=%CONFIG% --indent-labels +set CONFIG=%CONFIG% --pad-header +set CONFIG=%CONFIG% --pad-oper +set CONFIG=%CONFIG% --unpad-paren +set CONFIG=%CONFIG% --keep-one-line-blocks +set CONFIG=%CONFIG% --keep-one-line-statements +set CONFIG=%CONFIG% --convert-tabs +set CONFIG=%CONFIG% --indent-preprocessor +set CONFIG=%CONFIG% --align-pointer=type +set CONFIG=%CONFIG% --suffix=none + +call astyle %CONFIG% !ROOT_DIR!\..\src\*.cpp +call astyle %CONFIG% !ROOT_DIR!\..\src\*.h diff --git a/tools/check-style.sh b/tools/check-style.sh new file mode 100755 index 0000000..019c2f2 --- /dev/null +++ b/tools/check-style.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +cwd=$(pwd) + +CONFIG='' +CONFIG+=' --indent=spaces=4' +CONFIG+=' --style=otbs' +CONFIG+=' --indent-labels' +CONFIG+=' --pad-header' +CONFIG+=' --pad-oper' +CONFIG+=' --unpad-paren' +CONFIG+=' --keep-one-line-blocks' +CONFIG+=' --keep-one-line-statements' +CONFIG+=' --convert-tabs' +CONFIG+=' --indent-preprocessor' +CONFIG+=' --align-pointer=type' +CONFIG+=' --suffix=none' + +astyle -n -Q $CONFIG $cwd/src/*.cpp +astyle -n -Q $CONFIG $cwd/src/*.h diff --git a/tools/import-linenoise.sh b/tools/import-linenoise.sh new file mode 100755 index 0000000..507fe3a --- /dev/null +++ b/tools/import-linenoise.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +LINENOISE_PATH="$PWD/../src/linenoise" +LINENOISE_SRC_PATH="$LINENOISE_PATH/src" +GITHUB_CLONE_URL="http://github.com/tadmarshall/linenoise.git" +TO_REMOVE=".gitignore .git *.vcproj *.sln Makefile" + + +# Make a new Linenoise source directory +rm -rf $LINENOISE_SRC_PATH +mkdir -p $LINENOISE_SRC_PATH + +# Cloning latest 'master' of Linenoise +git clone $GITHUB_CLONE_URL $LINENOISE_SRC_PATH + +# From within the source directory... +pushd $LINENOISE_SRC_PATH + +# Extract latest commit log info and prepare "README.md" content +LATEST_COMMIT=$(git log -1) +README_CONTENT=$(cat << EOF +This project contains the **Linenoise project**, initially released +by [Salvatore Sanfilippo](https://github.com/antirez). Here we import a fork +by [Tad Marshall](https://github.com/tadmarshall) that lives at +[github.com/tadmarshall/linenoise](https://github.com/tadmarshall/linenoise). + +The version of Linenoise included in PhantomJS refers to the commit: +----- +$LATEST_COMMIT +----- + +Some files not needed for PhantomJS are removed. + +Linenoise is licensed under the BSD-license. +Kudos to all the developers that contribute to this nice little pearl. + +EOF) + +# Remove unnecessary files +rm -rf $TO_REMOVE + +popd # ... and out! + +# Save "README.md" +echo "$README_CONTENT" > "$LINENOISE_PATH/README.md" diff --git a/tools/preconfig.sh b/tools/preconfig.sh new file mode 100755 index 0000000..9d36c7d --- /dev/null +++ b/tools/preconfig.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +COMPILE_JOBS=4 + +QT_CFG='' +QT_CFG+=' -opensource' # Use the open-source license +QT_CFG+=' -confirm-license' # Silently acknowledge the license confirmation +QT_CFG+=' -v' # Makes it easier to see what header dependencies are missing + +if [[ $OSTYPE = darwin* ]]; then + QT_CFG+=' -static' # Static build on Mac OS X only + QT_CFG+=' -arch x86' + QT_CFG+=' -cocoa' # Cocoa only, ignore Carbon + QT_CFG+=' -no-dwarf2' +else + QT_CFG+=' -system-freetype' # Freetype for text rendering + QT_CFG+=' -fontconfig' # Fontconfig for better font matching + QT_CFG+=' -qpa' # X11-less with QPA (aka Lighthouse) +fi + +QT_CFG+=' -release' # Build only for release (no debugging support) +QT_CFG+=' -fast' # Accelerate Makefiles generation +QT_CFG+=' -nomake demos' # Don't build with the demos +QT_CFG+=' -nomake docs' # Don't generate the documentation +QT_CFG+=' -nomake examples' # Don't build any examples +QT_CFG+=' -nomake translations' # Ignore the translations +QT_CFG+=' -nomake tools' # Don't build the tools + +QT_CFG+=' -no-exceptions' # Don't use C++ exception +QT_CFG+=' -no-stl' # No need for STL compatibility + +# Irrelevant Qt features +QT_CFG+=' -no-libmng' +QT_CFG+=' -no-libtiff' +QT_CFG+=' -no-icu' + +# Unnecessary Qt modules +QT_CFG+=' -no-declarative' +QT_CFG+=' -no-multimedia' +QT_CFG+=' -no-opengl' +QT_CFG+=' -no-openvg' +QT_CFG+=' -no-phonon' +QT_CFG+=' -no-qt3support' +QT_CFG+=' -no-script' +QT_CFG+=' -no-scripttools' +QT_CFG+=' -no-svg' +QT_CFG+=' -no-xmlpatterns' + +# Unnecessary Qt features +QT_CFG+=' -D QT_NO_GRAPHICSVIEW' +QT_CFG+=' -D QT_NO_GRAPHICSEFFECT' + +# Sets the default graphics system to the raster engine +QT_CFG+=' -graphicssystem raster' + +# Unix +QT_CFG+=' -no-dbus' # Disable D-Bus feature +QT_CFG+=' -no-glib' # No need for Glib integration +QT_CFG+=' -no-gstreamer' # Turn off GStreamer support +QT_CFG+=' -no-gtkstyle' # Disable theming integration with Gtk+ +QT_CFG+=' -no-cups' # Disable CUPs support +QT_CFG+=' -no-sm' +QT_CFG+=' -no-xinerama' +QT_CFG+=' -no-xkb' + +# Use the bundled libraries, vs system-installed +QT_CFG+=' -qt-libjpeg' +QT_CFG+=' -qt-libpng' +QT_CFG+=' -qt-zlib' + +# Explicitly compile with SSL support, so build will fail if headers are missing +QT_CFG+=' -openssl' + +# Useless styles +QT_CFG+=' -D QT_NO_STYLESHEET' +QT_CFG+=' -D QT_NO_STYLE_CDE' +QT_CFG+=' -D QT_NO_STYLE_CLEANLOOKS' +QT_CFG+=' -D QT_NO_STYLE_MOTIF' +QT_CFG+=' -D QT_NO_STYLE_PLASTIQUE' + +until [ -z "$1" ]; do + case $1 in + "--qt-config") + shift + QT_CFG+=" $1" + shift;; + "--jobs") + shift + COMPILE_JOBS=$1 + shift;; + "--help") + echo "Usage: $0 [--qt-config CONFIG] [--jobs NUM]" + echo + echo " --qt-config CONFIG Specify extra config options to be used when configuring Qt" + echo " --jobs NUM How many parallel compile jobs to use. Defaults to 4." + echo + exit 0 + ;; + *) + echo "Unrecognised option: $1" + exit 1;; + esac +done + + +# For parallelizing the bootstrapping process, e.g. qmake and friends. +export MAKEFLAGS=-j$COMPILE_JOBS + +./configure -prefix $PWD $QT_CFG +make -j$COMPILE_JOBS + +# Extra step to ensure the static libraries are found +cp -rp src/3rdparty/webkit/Source/JavaScriptCore/release/* lib/ +cp -rp src/3rdparty/webkit/Source/WebCore/release/* lib/