├── .rspec ├── lib ├── capybara-webkit.rb ├── capybara │ ├── driver │ │ ├── webkit │ │ │ ├── version.rb │ │ │ ├── socket_debugger.rb │ │ │ ├── cookie_jar.rb │ │ │ ├── node.rb │ │ │ ├── connection.rb │ │ │ └── browser.rb │ │ └── webkit.rb │ ├── webkit.rb │ └── webkit │ │ └── matchers.rb └── capybara_webkit_builder.rb ├── Gemfile ├── webkit_server.pro ├── src ├── webkit_server.qrc ├── Url.h ├── Body.h ├── Header.h ├── SetProxy.h ├── Find.h ├── SetCookie.h ├── Status.h ├── Visit.h ├── Execute.h ├── Headers.h ├── ClearCookies.h ├── NullCommand.h ├── RequestedUrl.h ├── ResizeWindow.h ├── Node.h ├── Render.h ├── ConsoleMessages.h ├── IgnoreSslErrors.h ├── Headers.cpp ├── GetCookies.h ├── Reset.h ├── body.cpp ├── ConsoleMessages.cpp ├── IgnoreSslErrors.cpp ├── CommandFactory.h ├── Status.cpp ├── CurrentUrl.h ├── Url.cpp ├── Response.h ├── Source.h ├── NullCommand.cpp ├── Server.h ├── Command.cpp ├── NetworkCookieJar.h ├── RequestedUrl.cpp ├── Visit.cpp ├── JavascriptInvocation.cpp ├── ResizeWindow.cpp ├── UnsupportedContentHandler.h ├── Evaluate.h ├── Node.cpp ├── Render.cpp ├── Execute.cpp ├── Response.cpp ├── JavascriptInvocation.h ├── Find.cpp ├── Source.cpp ├── FrameFocus.h ├── Command.h ├── ClearCookies.cpp ├── NetworkAccessManager.h ├── Header.cpp ├── SetCookie.cpp ├── Server.cpp ├── GetCookies.cpp ├── SetProxy.cpp ├── CommandParser.h ├── find_command.h ├── main.cpp ├── Connection.h ├── Reset.cpp ├── CommandFactory.cpp ├── PageLoadingCommand.h ├── NetworkAccessManager.cpp ├── UnsupportedContentHandler.cpp ├── PageLoadingCommand.cpp ├── FrameFocus.cpp ├── webkit_server.pro ├── CommandParser.cpp ├── WebPage.h ├── Connection.cpp ├── Evaluate.cpp ├── CurrentUrl.cpp ├── NetworkCookieJar.cpp ├── WebPage.cpp └── capybara.js ├── Appraisals ├── extconf.rb ├── gemfiles ├── 1.0.gemfile ├── 1.1.gemfile ├── 1.0.gemfile.lock └── 1.1.gemfile.lock ├── templates ├── Command.cpp └── Command.h ├── .gitignore ├── spec ├── integration │ ├── driver_spec.rb │ └── session_spec.rb ├── capybara_webkit_builder_spec.rb ├── spec_helper.rb ├── cookie_jar_spec.rb ├── driver_resize_window_spec.rb ├── connection_spec.rb ├── self_signed_ssl_cert.rb ├── driver_rendering_spec.rb ├── browser_spec.rb └── driver_spec.rb ├── bin └── Info.plist ├── LICENSE ├── NEWS.md ├── capybara-webkit.gemspec ├── Gemfile.lock ├── CONTRIBUTING.md ├── Rakefile ├── ChangeLog └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /lib/capybara-webkit.rb: -------------------------------------------------------------------------------- 1 | require "capybara/webkit" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /webkit_server.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | CONFIG += ordered 3 | SUBDIRS += src/webkit_server.pro 4 | 5 | -------------------------------------------------------------------------------- /src/webkit_server.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | capybara.js 4 | 5 | 6 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "1.0" do 2 | gem "capybara", "~> 1.0.0" 3 | end 4 | 5 | appraise "1.1" do 6 | gem "capybara", "~> 1.1.0" 7 | end 8 | -------------------------------------------------------------------------------- /extconf.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.expand_path(File.dirname(__FILE__)), "lib", "capybara_webkit_builder") 2 | CapybaraWebkitBuilder.build_all 3 | -------------------------------------------------------------------------------- /gemfiles/1.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "capybara", "~> 1.0.0" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/1.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "capybara", "~> 1.1.0" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/version.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Driver 3 | class Webkit 4 | VERSION = '0.11.0'.freeze 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /templates/Command.cpp: -------------------------------------------------------------------------------- 1 | #include "NAME.h" 2 | #include "WebPage.h" 3 | 4 | NAME::NAME(WebPage *page, QObject *parent) : Command(page, parent) { 5 | } 6 | 7 | void NAME::start(QStringList &arguments) { 8 | Q_UNUSED(arguments); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/Url.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Url : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Url(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Body.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Body : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Body(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Header.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Header : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Header(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/SetProxy.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class SetProxy : public Command { 6 | Q_OBJECT; 7 | 8 | public: 9 | SetProxy(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/Find.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Find : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Find(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/SetCookie.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class SetCookie : public Command { 6 | Q_OBJECT; 7 | 8 | public: 9 | SetCookie(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/Status.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Status : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Status(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Visit.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Visit : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Visit(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /templates/Command.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class NAME : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | NAME(WebPage *page, QObject *parent = 0); 10 | virtual void start(QStringList &arguments); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | bin/webkit_server* 3 | *.swo 4 | *~ 5 | *.o 6 | *.moc 7 | Makefile* 8 | qrc_* 9 | *.xcodeproj 10 | *.app 11 | moc_*.cpp 12 | .bundle 13 | pkg 14 | src/webkit_server 15 | src/webkit_server.exe 16 | .DS_Store 17 | tmp 18 | .rvmrc 19 | src/debug -------------------------------------------------------------------------------- /src/Execute.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Execute : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Execute(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Headers.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Headers : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Headers(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/ClearCookies.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class ClearCookies : public Command { 6 | Q_OBJECT; 7 | 8 | public: 9 | ClearCookies(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/NullCommand.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class NullCommand : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | NullCommand(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/RequestedUrl.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class RequestedUrl : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | RequestedUrl(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/ResizeWindow.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class ResizeWindow : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | ResizeWindow(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Node.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | #include 3 | 4 | class WebPage; 5 | 6 | class Node : public Command { 7 | Q_OBJECT 8 | 9 | public: 10 | Node(WebPage *page, QStringList &arguments, QObject *parent = 0); 11 | virtual void start(); 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /src/Render.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | #include 3 | 4 | class WebPage; 5 | 6 | class Render : public Command { 7 | Q_OBJECT 8 | 9 | public: 10 | Render(WebPage *page, QStringList &arguments, QObject *parent = 0); 11 | virtual void start(); 12 | }; 13 | -------------------------------------------------------------------------------- /src/ConsoleMessages.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class ConsoleMessages : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | ConsoleMessages(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/IgnoreSslErrors.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class IgnoreSslErrors : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | IgnoreSslErrors(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/Headers.cpp: -------------------------------------------------------------------------------- 1 | #include "Headers.h" 2 | #include "WebPage.h" 3 | 4 | Headers::Headers(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Headers::start() { 8 | emit finished(new Response(true, page()->pageHeaders())); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/GetCookies.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class GetCookies : public Command { 6 | Q_OBJECT; 7 | 8 | public: 9 | GetCookies(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | 12 | private: 13 | QString m_buffer; 14 | }; 15 | -------------------------------------------------------------------------------- /src/Reset.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class Reset : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | Reset(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | 12 | private: 13 | void resetHistory(); 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /src/body.cpp: -------------------------------------------------------------------------------- 1 | #include "Body.h" 2 | #include "WebPage.h" 3 | 4 | Body::Body(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Body::start() { 8 | QString result = page()->currentFrame()->toHtml(); 9 | emit finished(new Response(true, result)); 10 | } 11 | -------------------------------------------------------------------------------- /src/ConsoleMessages.cpp: -------------------------------------------------------------------------------- 1 | #include "ConsoleMessages.h" 2 | #include "WebPage.h" 3 | 4 | ConsoleMessages::ConsoleMessages(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void ConsoleMessages::start() { 8 | emit finished(new Response(true, page()->consoleMessages())); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/IgnoreSslErrors.cpp: -------------------------------------------------------------------------------- 1 | #include "IgnoreSslErrors.h" 2 | #include "WebPage.h" 3 | 4 | IgnoreSslErrors::IgnoreSslErrors(WebPage *page, QStringList &arguments, QObject *parent) : 5 | Command(page, arguments, parent) { 6 | } 7 | 8 | void IgnoreSslErrors::start() { 9 | page()->ignoreSslErrors(); 10 | emit finished(new Response(true)); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/CommandFactory.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class Command; 4 | class WebPage; 5 | 6 | class CommandFactory : public QObject { 7 | Q_OBJECT 8 | 9 | public: 10 | CommandFactory(WebPage *page, QObject *parent = 0); 11 | Command *createCommand(const char *name, QStringList &arguments); 12 | 13 | private: 14 | WebPage *m_page; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /src/Status.cpp: -------------------------------------------------------------------------------- 1 | #include "Status.h" 2 | #include "WebPage.h" 3 | #include 4 | 5 | Status::Status(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | } 7 | 8 | void Status::start() { 9 | int status = page()->getLastStatus(); 10 | emit finished(new Response(true, QString::number(status))); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/CurrentUrl.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | 5 | class CurrentUrl : public Command { 6 | Q_OBJECT 7 | 8 | public: 9 | CurrentUrl(WebPage *page, QStringList &arguments, QObject *parent = 0); 10 | virtual void start(); 11 | 12 | private: 13 | bool wasRegularLoad(); 14 | bool wasRedirectedAndNotModifiedByJavascript(); 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /src/Url.cpp: -------------------------------------------------------------------------------- 1 | #include "Url.h" 2 | #include "WebPage.h" 3 | 4 | Url::Url(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Url::start() { 8 | QUrl humanUrl = page()->currentFrame()->url(); 9 | QByteArray encodedBytes = humanUrl.toEncoded(); 10 | emit finished(new Response(true, encodedBytes)); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/Response.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class Response { 5 | public: 6 | Response(bool success, QString message); 7 | Response(bool success, QByteArray message); 8 | Response(bool success); 9 | bool isSuccess() const; 10 | QByteArray message() const; 11 | 12 | private: 13 | bool m_success; 14 | QByteArray m_message; 15 | }; 16 | -------------------------------------------------------------------------------- /src/Source.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | class QNetworkReply; 5 | 6 | class Source : public Command { 7 | Q_OBJECT 8 | 9 | public: 10 | Source(WebPage *page, QStringList &arguments, QObject *parent = 0); 11 | virtual void start(); 12 | 13 | public slots: 14 | void sourceLoaded(); 15 | 16 | private: 17 | QNetworkReply *reply; 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /src/NullCommand.cpp: -------------------------------------------------------------------------------- 1 | #include "NullCommand.h" 2 | #include "WebPage.h" 3 | 4 | NullCommand::NullCommand(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) {} 5 | 6 | void NullCommand::start() { 7 | QString failure = QString("[Capybara WebKit] Unknown command: ") + arguments()[0] + "\n"; 8 | emit finished(new Response(false, failure)); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/Server.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class QTcpServer; 4 | class WebPage; 5 | 6 | class Server : public QObject { 7 | Q_OBJECT 8 | 9 | public: 10 | Server(QObject *parent); 11 | bool start(); 12 | quint16 server_port() const; 13 | 14 | public slots: 15 | void handleConnection(); 16 | 17 | private: 18 | QTcpServer *m_tcp_server; 19 | WebPage *m_page; 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /lib/capybara/webkit.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/driver/webkit" 3 | 4 | Capybara.register_driver :webkit do |app| 5 | Capybara::Driver::Webkit.new(app) 6 | end 7 | 8 | Capybara.register_driver :webkit_debug do |app| 9 | browser = Capybara::Driver::Webkit::Browser.new(:socket_class => Capybara::Driver::Webkit::SocketDebugger) 10 | Capybara::Driver::Webkit.new(app, :browser => browser) 11 | end 12 | -------------------------------------------------------------------------------- /src/Command.cpp: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | #include "WebPage.h" 3 | 4 | Command::Command(WebPage *page, QStringList &arguments, QObject *parent) : QObject(parent) { 5 | m_page = page; 6 | m_arguments = arguments; 7 | } 8 | 9 | void Command::start() { 10 | } 11 | 12 | WebPage *Command::page() { 13 | return m_page; 14 | } 15 | 16 | QStringList &Command::arguments() { 17 | return m_arguments; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/NetworkCookieJar.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class NetworkCookieJar : public QNetworkCookieJar { 5 | 6 | Q_OBJECT; 7 | 8 | public: 9 | 10 | NetworkCookieJar(QObject *parent = 0); 11 | 12 | QList getAllCookies() const; 13 | void clearCookies(); 14 | void overwriteCookies(const QList& cookieList); 15 | }; 16 | -------------------------------------------------------------------------------- /src/RequestedUrl.cpp: -------------------------------------------------------------------------------- 1 | #include "RequestedUrl.h" 2 | #include "WebPage.h" 3 | 4 | RequestedUrl::RequestedUrl(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void RequestedUrl::start() { 8 | QUrl humanUrl = page()->currentFrame()->requestedUrl(); 9 | QByteArray encodedBytes = humanUrl.toEncoded(); 10 | emit finished(new Response(true, encodedBytes)); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/Visit.cpp: -------------------------------------------------------------------------------- 1 | #include "Visit.h" 2 | #include "Command.h" 3 | #include "WebPage.h" 4 | 5 | Visit::Visit(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | } 7 | 8 | void Visit::start() { 9 | QUrl requestedUrl = QUrl::fromEncoded(arguments()[0].toUtf8(), QUrl::StrictMode); 10 | page()->currentFrame()->load(QUrl(requestedUrl)); 11 | emit finished(new Response(true)); 12 | } 13 | -------------------------------------------------------------------------------- /src/JavascriptInvocation.cpp: -------------------------------------------------------------------------------- 1 | #include "JavascriptInvocation.h" 2 | 3 | JavascriptInvocation::JavascriptInvocation(QString &functionName, QStringList &arguments, QObject *parent) : QObject(parent) { 4 | m_functionName = functionName; 5 | m_arguments = arguments; 6 | } 7 | 8 | QString &JavascriptInvocation::functionName() { 9 | return m_functionName; 10 | } 11 | 12 | QStringList &JavascriptInvocation::arguments() { 13 | return m_arguments; 14 | } 15 | -------------------------------------------------------------------------------- /src/ResizeWindow.cpp: -------------------------------------------------------------------------------- 1 | #include "ResizeWindow.h" 2 | #include "WebPage.h" 3 | 4 | ResizeWindow::ResizeWindow(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void ResizeWindow::start() { 8 | int width = arguments()[0].toInt(); 9 | int height = arguments()[1].toInt(); 10 | 11 | QSize size(width, height); 12 | page()->setViewportSize(size); 13 | 14 | emit finished(new Response(true)); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/UnsupportedContentHandler.h: -------------------------------------------------------------------------------- 1 | #include 2 | class WebPage; 3 | class QNetworkReply; 4 | class UnsupportedContentHandler : public QObject { 5 | Q_OBJECT 6 | 7 | public: 8 | UnsupportedContentHandler(WebPage *page, QNetworkReply *reply, QObject *parent = 0); 9 | 10 | public slots: 11 | void handleUnsupportedContent(); 12 | 13 | private: 14 | WebPage *m_page; 15 | QNetworkReply *m_reply; 16 | void loadUnsupportedContent(); 17 | void finish(bool success); 18 | }; 19 | -------------------------------------------------------------------------------- /src/Evaluate.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | #include 4 | 5 | class WebPage; 6 | 7 | class Evaluate : public Command { 8 | Q_OBJECT 9 | 10 | public: 11 | Evaluate(WebPage *page, QStringList &arguments, QObject *parent = 0); 12 | virtual void start(); 13 | 14 | private: 15 | void addVariant(QVariant &object); 16 | void addString(QString &string); 17 | void addArray(QVariantList &list); 18 | void addMap(QVariantMap &map); 19 | 20 | QString m_buffer; 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/Node.cpp: -------------------------------------------------------------------------------- 1 | #include "Node.h" 2 | #include "WebPage.h" 3 | 4 | Node::Node(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Node::start() { 8 | QStringList functionArguments(arguments()); 9 | QString functionName = functionArguments.takeFirst(); 10 | QVariant result = page()->invokeCapybaraFunction(functionName, functionArguments); 11 | QString attributeValue = result.toString(); 12 | emit finished(new Response(true, attributeValue)); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/Render.cpp: -------------------------------------------------------------------------------- 1 | #include "Render.h" 2 | #include "WebPage.h" 3 | 4 | Render::Render(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Render::start() { 8 | QString imagePath = arguments()[0]; 9 | int width = arguments()[1].toInt(); 10 | int height = arguments()[2].toInt(); 11 | 12 | QSize size(width, height); 13 | page()->setViewportSize(size); 14 | 15 | bool result = page()->render( imagePath ); 16 | 17 | emit finished(new Response(result)); 18 | } 19 | -------------------------------------------------------------------------------- /src/Execute.cpp: -------------------------------------------------------------------------------- 1 | #include "Execute.h" 2 | #include "WebPage.h" 3 | 4 | Execute::Execute(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Execute::start() { 8 | QString script = arguments()[0] + QString("; 'success'"); 9 | QVariant result = page()->currentFrame()->evaluateJavaScript(script); 10 | if (result.isValid()) { 11 | emit finished(new Response(true)); 12 | } else { 13 | emit finished(new Response(false, QString("Javascript failed to execute"))); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/Response.cpp: -------------------------------------------------------------------------------- 1 | #include "Response.h" 2 | #include 3 | 4 | Response::Response(bool success, QString message) { 5 | m_success = success; 6 | m_message = message.toUtf8(); 7 | } 8 | 9 | Response::Response(bool success, QByteArray message) { 10 | m_success = success; 11 | m_message = message; 12 | } 13 | 14 | Response::Response(bool success) { 15 | m_success = success; 16 | } 17 | 18 | bool Response::isSuccess() const { 19 | return m_success; 20 | } 21 | 22 | QByteArray Response::message() const { 23 | return m_message; 24 | } 25 | -------------------------------------------------------------------------------- /src/JavascriptInvocation.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class JavascriptInvocation : public QObject { 6 | Q_OBJECT 7 | Q_PROPERTY(QString functionName READ functionName) 8 | Q_PROPERTY(QStringList arguments READ arguments) 9 | 10 | public: 11 | JavascriptInvocation(QString &functionName, QStringList &arguments, QObject *parent = 0); 12 | QString &functionName(); 13 | QStringList &arguments(); 14 | 15 | private: 16 | QString m_functionName; 17 | QStringList m_arguments; 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /src/Find.cpp: -------------------------------------------------------------------------------- 1 | #include "Find.h" 2 | #include "Command.h" 3 | #include "WebPage.h" 4 | 5 | Find::Find(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | } 7 | 8 | void Find::start() { 9 | QString message; 10 | QVariant result = page()->invokeCapybaraFunction("find", arguments()); 11 | 12 | if (result.isValid()) { 13 | message = result.toString(); 14 | emit finished(new Response(true, message)); 15 | } else { 16 | emit finished(new Response(false, QString("Invalid XPath expression"))); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Source.cpp: -------------------------------------------------------------------------------- 1 | #include "Source.h" 2 | #include "WebPage.h" 3 | 4 | Source::Source(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | void Source::start() { 8 | QNetworkAccessManager* accessManager = page()->networkAccessManager(); 9 | QNetworkRequest request(page()->currentFrame()->url()); 10 | reply = accessManager->get(request); 11 | 12 | connect(reply, SIGNAL(finished()), this, SLOT(sourceLoaded())); 13 | } 14 | 15 | void Source::sourceLoaded() { 16 | emit finished(new Response(true, reply->readAll())); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/FrameFocus.h: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | class WebPage; 4 | class QWebFrame; 5 | 6 | class FrameFocus : public Command { 7 | Q_OBJECT 8 | 9 | public: 10 | FrameFocus(WebPage *page, QStringList &arguments, QObject *parent = 0); 11 | virtual void start(); 12 | 13 | private: 14 | void findFrames(); 15 | 16 | void focusParent(); 17 | 18 | void focusIndex(int index); 19 | bool isFrameAtIndex(int index); 20 | 21 | void focusId(QString id); 22 | 23 | void success(); 24 | void frameNotFound(); 25 | 26 | QList frames; 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /src/Command.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_H 2 | #define COMMAND_H 3 | 4 | #include 5 | #include 6 | #include "Response.h" 7 | 8 | class WebPage; 9 | 10 | class Command : public QObject { 11 | Q_OBJECT 12 | 13 | public: 14 | Command(WebPage *page, QStringList &arguments, QObject *parent = 0); 15 | virtual void start(); 16 | 17 | signals: 18 | void finished(Response *response); 19 | 20 | protected: 21 | WebPage *page(); 22 | QStringList &arguments(); 23 | 24 | private: 25 | WebPage *m_page; 26 | QStringList m_arguments; 27 | 28 | }; 29 | 30 | #endif 31 | 32 | -------------------------------------------------------------------------------- /src/ClearCookies.cpp: -------------------------------------------------------------------------------- 1 | #include "ClearCookies.h" 2 | #include "WebPage.h" 3 | #include "NetworkCookieJar.h" 4 | #include 5 | 6 | ClearCookies::ClearCookies(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) {} 7 | 8 | void ClearCookies::start() 9 | { 10 | NetworkCookieJar *jar = qobject_cast(page() 11 | ->networkAccessManager() 12 | ->cookieJar()); 13 | jar->clearCookies(); 14 | emit finished(new Response(true)); 15 | } 16 | -------------------------------------------------------------------------------- /src/NetworkAccessManager.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class NetworkAccessManager : public QNetworkAccessManager { 6 | 7 | Q_OBJECT 8 | 9 | public: 10 | NetworkAccessManager(QObject *parent = 0); 11 | void addHeader(QString key, QString value); 12 | void resetHeaders(); 13 | 14 | protected: 15 | QNetworkReply* createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice * outgoingData); 16 | 17 | private: 18 | QHash m_headers; 19 | }; 20 | -------------------------------------------------------------------------------- /src/Header.cpp: -------------------------------------------------------------------------------- 1 | #include "Header.h" 2 | #include "WebPage.h" 3 | #include "NetworkAccessManager.h" 4 | 5 | Header::Header(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | } 7 | 8 | void Header::start() { 9 | QString key = arguments()[0]; 10 | QString value = arguments()[1]; 11 | NetworkAccessManager* networkAccessManager = qobject_cast(page()->networkAccessManager()); 12 | if (key.toLower().replace("-", "_") == "user_agent") { 13 | page()->setUserAgent(value); 14 | } else { 15 | networkAccessManager->addHeader(key, value); 16 | } 17 | emit finished(new Response(true)); 18 | } 19 | -------------------------------------------------------------------------------- /src/SetCookie.cpp: -------------------------------------------------------------------------------- 1 | #include "SetCookie.h" 2 | #include "WebPage.h" 3 | #include "NetworkCookieJar.h" 4 | #include 5 | 6 | SetCookie::SetCookie(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) {} 7 | 8 | void SetCookie::start() 9 | { 10 | QList cookies = QNetworkCookie::parseCookies(arguments()[0].toAscii()); 11 | NetworkCookieJar *jar = qobject_cast(page() 12 | ->networkAccessManager() 13 | ->cookieJar()); 14 | jar->overwriteCookies(cookies); 15 | emit finished(new Response(true)); 16 | } 17 | -------------------------------------------------------------------------------- /src/Server.cpp: -------------------------------------------------------------------------------- 1 | #include "Server.h" 2 | #include "WebPage.h" 3 | #include "Connection.h" 4 | 5 | #include 6 | 7 | Server::Server(QObject *parent) : QObject(parent) { 8 | m_tcp_server = new QTcpServer(this); 9 | m_page = new WebPage(this); 10 | } 11 | 12 | bool Server::start() { 13 | connect(m_tcp_server, SIGNAL(newConnection()), this, SLOT(handleConnection())); 14 | return m_tcp_server->listen(QHostAddress::LocalHost, 0); 15 | } 16 | 17 | quint16 Server::server_port() const { 18 | return m_tcp_server->serverPort(); 19 | } 20 | 21 | void Server::handleConnection() { 22 | QTcpSocket *socket = m_tcp_server->nextPendingConnection(); 23 | new Connection(socket, m_page, this); 24 | } 25 | -------------------------------------------------------------------------------- /src/GetCookies.cpp: -------------------------------------------------------------------------------- 1 | #include "GetCookies.h" 2 | #include "WebPage.h" 3 | #include "NetworkCookieJar.h" 4 | 5 | GetCookies::GetCookies(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) 6 | { 7 | m_buffer = ""; 8 | } 9 | 10 | void GetCookies::start() 11 | { 12 | NetworkCookieJar *jar = qobject_cast(page() 13 | ->networkAccessManager() 14 | ->cookieJar()); 15 | foreach (QNetworkCookie cookie, jar->getAllCookies()) { 16 | m_buffer.append(cookie.toRawForm()); 17 | m_buffer.append("\n"); 18 | } 19 | emit finished(new Response(true, m_buffer)); 20 | } 21 | -------------------------------------------------------------------------------- /spec/integration/driver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit' 3 | 4 | describe Capybara::Driver::Webkit do 5 | before do 6 | @driver = Capybara::Driver::Webkit.new(TestApp, :browser => $webkit_browser) 7 | end 8 | 9 | it_should_behave_like "driver" 10 | it_should_behave_like "driver with javascript support" 11 | it_should_behave_like "driver with cookies support" 12 | it_should_behave_like "driver with header support" 13 | it_should_behave_like "driver with status code support" 14 | it_should_behave_like "driver with frame support" 15 | 16 | it "returns the rack server port" do 17 | @driver.server_port.should eq(@driver.instance_variable_get(:@rack_server).port) 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /bin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIconFile 6 | 7 | CFBundlePackageType 8 | APPL 9 | CFBundleGetInfoString 10 | Created by Qt/QMake 11 | CFBundleSignature 12 | ???? 13 | CFBundleExecutable 14 | webkit_server 15 | CFBundleIdentifier 16 | com.yourcompany.webkit_server 17 | NOTE 18 | This file was generated by Qt/QMake. 19 | LSUIElement 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/SetProxy.cpp: -------------------------------------------------------------------------------- 1 | #include "SetProxy.h" 2 | #include "WebPage.h" 3 | #include 4 | #include 5 | 6 | SetProxy::SetProxy(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) {} 7 | 8 | void SetProxy::start() 9 | { 10 | // default to empty proxy 11 | QNetworkProxy proxy; 12 | 13 | if (arguments().size() > 0) 14 | proxy = QNetworkProxy(QNetworkProxy::HttpProxy, 15 | arguments()[0], 16 | (quint16)(arguments()[1].toInt()), 17 | arguments()[2], 18 | arguments()[3]); 19 | 20 | page()->networkAccessManager()->setProxy(proxy); 21 | emit finished(new Response(true)); 22 | } 23 | -------------------------------------------------------------------------------- /src/CommandParser.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class QIODevice; 5 | class CommandFactory; 6 | class Command; 7 | 8 | class CommandParser : public QObject { 9 | Q_OBJECT 10 | 11 | public: 12 | CommandParser(QIODevice *device, CommandFactory *commandFactory, QObject *parent = 0); 13 | 14 | public slots: 15 | void checkNext(); 16 | 17 | signals: 18 | void commandReady(Command *command); 19 | 20 | private: 21 | void readLine(); 22 | void readDataBlock(); 23 | void processNext(const char *line); 24 | void processArgument(const char *data); 25 | void reset(); 26 | QIODevice *m_device; 27 | QString m_commandName; 28 | QStringList m_arguments; 29 | int m_argumentsExpected; 30 | int m_expectingDataSize; 31 | CommandFactory *m_commandFactory; 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/find_command.h: -------------------------------------------------------------------------------- 1 | #define CHECK_COMMAND(expectedName) \ 2 | if (strcmp(#expectedName, name) == 0) { \ 3 | return new expectedName(m_page, arguments, this); \ 4 | } 5 | 6 | CHECK_COMMAND(Visit) 7 | CHECK_COMMAND(Find) 8 | CHECK_COMMAND(Reset) 9 | CHECK_COMMAND(Node) 10 | CHECK_COMMAND(Url) 11 | CHECK_COMMAND(Source) 12 | CHECK_COMMAND(Evaluate) 13 | CHECK_COMMAND(Execute) 14 | CHECK_COMMAND(FrameFocus) 15 | CHECK_COMMAND(Header) 16 | CHECK_COMMAND(Render) 17 | CHECK_COMMAND(Body) 18 | CHECK_COMMAND(Status) 19 | CHECK_COMMAND(Headers) 20 | CHECK_COMMAND(SetCookie) 21 | CHECK_COMMAND(ClearCookies) 22 | CHECK_COMMAND(GetCookies) 23 | CHECK_COMMAND(Headers) 24 | CHECK_COMMAND(SetProxy) 25 | CHECK_COMMAND(ConsoleMessages) 26 | CHECK_COMMAND(RequestedUrl) 27 | CHECK_COMMAND(CurrentUrl) 28 | CHECK_COMMAND(ResizeWindow) 29 | CHECK_COMMAND(IgnoreSslErrors) 30 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Server.h" 2 | #include 3 | #include 4 | #ifdef Q_OS_UNIX 5 | #include 6 | #endif 7 | 8 | int main(int argc, char **argv) { 9 | #ifdef Q_OS_UNIX 10 | if (setpgid(0, 0) < 0) { 11 | std::cerr << "Unable to set new process group." << std::endl; 12 | return 1; 13 | } 14 | #endif 15 | 16 | QApplication app(argc, argv); 17 | app.setApplicationName("capybara-webkit"); 18 | app.setOrganizationName("thoughtbot, inc"); 19 | app.setOrganizationDomain("thoughtbot.com"); 20 | 21 | QStringList args = app.arguments(); 22 | 23 | Server server(0); 24 | 25 | if (server.start()) { 26 | std::cout << "Capybara-webkit server started, listening on port: " << server.server_port() << std::endl; 27 | return app.exec(); 28 | } else { 29 | std::cerr << "Couldn't start capybara-webkit server" << std::endl; 30 | return 1; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/Connection.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class QTcpSocket; 5 | class WebPage; 6 | class Command; 7 | class Response; 8 | class CommandParser; 9 | class CommandFactory; 10 | class PageLoadingCommand; 11 | 12 | class Connection : public QObject { 13 | Q_OBJECT 14 | 15 | public: 16 | Connection(QTcpSocket *socket, WebPage *page, QObject *parent = 0); 17 | 18 | public slots: 19 | void commandReady(Command *command); 20 | void finishCommand(Response *response); 21 | void pendingLoadFinished(bool success); 22 | 23 | private: 24 | void startCommand(); 25 | void writeResponse(Response *response); 26 | void writePageLoadFailure(); 27 | 28 | QTcpSocket *m_socket; 29 | Command *m_queuedCommand; 30 | WebPage *m_page; 31 | CommandParser *m_commandParser; 32 | CommandFactory *m_commandFactory; 33 | PageLoadingCommand *m_runningCommand; 34 | bool m_pageSuccess; 35 | bool m_commandWaiting; 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /src/Reset.cpp: -------------------------------------------------------------------------------- 1 | #include "Reset.h" 2 | #include "WebPage.h" 3 | #include "NetworkAccessManager.h" 4 | #include "NetworkCookieJar.h" 5 | 6 | Reset::Reset(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 7 | } 8 | 9 | void Reset::start() { 10 | page()->triggerAction(QWebPage::Stop); 11 | 12 | NetworkAccessManager* networkAccessManager = qobject_cast(page()->networkAccessManager()); 13 | networkAccessManager->setCookieJar(new NetworkCookieJar()); 14 | networkAccessManager->resetHeaders(); 15 | 16 | page()->setUserAgent(NULL); 17 | page()->resetResponseHeaders(); 18 | page()->resetConsoleMessages(); 19 | page()->resetWindowSize(); 20 | resetHistory(); 21 | emit finished(new Response(true)); 22 | } 23 | 24 | void Reset::resetHistory() { 25 | // Clearing the history preserves the current history item, so set it to blank first. 26 | page()->currentFrame()->setUrl(QUrl("about:blank")); 27 | page()->history()->clear(); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /spec/capybara_webkit_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara_webkit_builder' 3 | 4 | describe CapybaraWebkitBuilder do 5 | let(:builder) { CapybaraWebkitBuilder } 6 | 7 | it "will use the env variable for #make_bin" do 8 | with_env_vars("MAKE" => "fake_make") do 9 | builder.make_bin.should == "fake_make" 10 | end 11 | end 12 | 13 | it "will use the env variable for #qmake_bin" do 14 | with_env_vars("QMAKE" => "fake_qmake") do 15 | builder.qmake_bin.should == "fake_qmake" 16 | end 17 | end 18 | 19 | it "will use the env variable for #os_spec" do 20 | with_env_vars("SPEC" => "fake_os_spec") do 21 | builder.spec.should == "fake_os_spec" 22 | end 23 | end 24 | 25 | it "defaults the #make_bin" do 26 | builder.make_bin.should == 'make' 27 | end 28 | 29 | it "defaults the #qmake_bin" do 30 | builder.qmake_bin.should == 'qmake' 31 | end 32 | 33 | it "defaults #spec to the #os_specs" do 34 | builder.spec.should == builder.os_spec 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/socket_debugger.rb: -------------------------------------------------------------------------------- 1 | # Wraps the TCP socket and prints data sent and received. Used for debugging 2 | # the wire protocol. You can use this by passing a :socket_class to Browser. 3 | class Capybara::Driver::Webkit 4 | class SocketDebugger 5 | def self.open(host, port) 6 | real_socket = TCPSocket.open(host, port) 7 | new(real_socket) 8 | end 9 | 10 | def initialize(socket) 11 | @socket = socket 12 | end 13 | 14 | def read(length) 15 | received @socket.read(length) 16 | end 17 | 18 | def puts(line) 19 | sent line 20 | @socket.puts(line) 21 | end 22 | 23 | def print(content) 24 | sent content 25 | @socket.print(content) 26 | end 27 | 28 | def gets 29 | received @socket.gets 30 | end 31 | 32 | private 33 | 34 | def sent(content) 35 | Kernel.puts " >> " + content.to_s 36 | end 37 | 38 | def received(content) 39 | Kernel.puts " << " + content.to_s 40 | content 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/CommandFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "CommandFactory.h" 2 | #include "NullCommand.h" 3 | #include "Visit.h" 4 | #include "Find.h" 5 | #include "Command.h" 6 | #include "Reset.h" 7 | #include "Node.h" 8 | #include "Url.h" 9 | #include "Source.h" 10 | #include "Evaluate.h" 11 | #include "Execute.h" 12 | #include "FrameFocus.h" 13 | #include "Header.h" 14 | #include "Render.h" 15 | #include "Body.h" 16 | #include "Status.h" 17 | #include "Headers.h" 18 | #include "SetCookie.h" 19 | #include "ClearCookies.h" 20 | #include "GetCookies.h" 21 | #include "SetProxy.h" 22 | #include "ConsoleMessages.h" 23 | #include "RequestedUrl.h" 24 | #include "CurrentUrl.h" 25 | #include "ResizeWindow.h" 26 | #include "IgnoreSslErrors.h" 27 | 28 | CommandFactory::CommandFactory(WebPage *page, QObject *parent) : QObject(parent) { 29 | m_page = page; 30 | } 31 | 32 | Command *CommandFactory::createCommand(const char *name, QStringList &arguments) { 33 | #include "find_command.h" 34 | arguments.clear(); 35 | arguments.append(QString(name)); 36 | return new NullCommand(m_page, arguments); 37 | } 38 | -------------------------------------------------------------------------------- /lib/capybara/webkit/matchers.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Webkit 3 | module RspecMatchers 4 | RSpec::Matchers.define :have_errors do |expected| 5 | match do |actual| 6 | actual = resolve(actual) 7 | actual.error_messages.any? 8 | end 9 | 10 | failure_message_for_should do |actual| 11 | "Expected Javascript errors, but there were none." 12 | end 13 | 14 | failure_message_for_should_not do |actual| 15 | actual = resolve(actual) 16 | "Expected no Javascript errors, got:\n#{error_messages_for(actual)}" 17 | end 18 | 19 | def error_messages_for(obj) 20 | obj.error_messages.map do |m| 21 | " - #{m[:message]}" 22 | end.join("\n") 23 | end 24 | 25 | def resolve(actual) 26 | if actual.respond_to? :page 27 | actual.page.driver 28 | elsif actual.respond_to? :driver 29 | actual.driver 30 | else 31 | actual 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 thoughtbot, inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/PageLoadingCommand.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | class Command; 5 | class Response; 6 | class WebPage; 7 | 8 | /* 9 | * Decorates a Command by deferring the finished() signal until any pending 10 | * page loads are complete. 11 | * 12 | * If a Command starts a page load, no signal will be emitted until the page 13 | * load is finished. 14 | * 15 | * If a pending page load fails, the command's response will be discarded and a 16 | * failure response will be emitted instead. 17 | */ 18 | class PageLoadingCommand : public QObject { 19 | Q_OBJECT 20 | 21 | public: 22 | PageLoadingCommand(Command *command, WebPage *page, QObject *parent = 0); 23 | void start(); 24 | 25 | public slots: 26 | void pageLoadingFromCommand(); 27 | void pendingLoadFinished(bool success); 28 | void commandFinished(Response *response); 29 | 30 | signals: 31 | void finished(Response *response); 32 | 33 | private: 34 | WebPage *m_page; 35 | Command *m_command; 36 | Response *m_pendingResponse; 37 | bool m_pageSuccess; 38 | bool m_pageLoadingFromCommand; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | New for 0.11.0: 2 | 3 | * Allow interaction with invisible elements 4 | * Use Timeout from stdlib since Capybara.timeout is being removed 5 | 6 | New for 0.10.1: 7 | 8 | * LANG environment variable is set to en_US.UTF-8 in order to avoid string encoding issues from qmake. 9 | * pro, find_command, and CommandFactory are more structured. 10 | * Changed wiki link and directing platform specific issues to the google group. 11 | * Pass proper keycode value for keypress events. 12 | 13 | New for 0.10.0: 14 | 15 | * current_url now more closely matches the behavior of Selenium 16 | * custom MAKE, QMAKE, and SPEC options can be set from the environment 17 | * BUG: Selected attribute is no longer removed when selecting/deselecting. Only the property is changed. 18 | 19 | New for 0.9.0: 20 | 21 | * Raise an error when an invisible element receives #click. 22 | * Raise ElementNotDisplayedError for #drag_to and #select_option when element is invisible. 23 | * Trigger mousedown and mouseup events. 24 | * Model mouse events more closely to the browser. 25 | * Try to detech when a command starts a page load and wait for it to finish 26 | -------------------------------------------------------------------------------- /capybara-webkit.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "capybara/driver/webkit/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "capybara-webkit" 6 | s.version = Capybara::Driver::Webkit::VERSION.dup 7 | s.authors = ["thoughtbot", "Joe Ferris", "Matt Mongeau", "Mike Burns", "Jason Morrison"] 8 | s.email = "support@thoughtbot.com" 9 | s.homepage = "http://github.com/thoughtbot/capybara-webkit" 10 | s.summary = "Headless Webkit driver for Capybara" 11 | 12 | s.files = `git ls-files`.split("\n") 13 | s.test_files = `git ls-files -- {spec,features}/*`.split("\n") 14 | s.require_path = "lib" 15 | 16 | s.extensions = "extconf.rb" 17 | 18 | s.add_runtime_dependency("capybara", [">= 1.0.0", "< 1.2"]) 19 | s.add_runtime_dependency("json") 20 | 21 | s.add_development_dependency("rspec", "~> 2.6.0") 22 | # Sinatra is used by Capybara's TestApp 23 | s.add_development_dependency("sinatra") 24 | s.add_development_dependency("mini_magick") 25 | s.add_development_dependency("rake") 26 | s.add_development_dependency("appraisal", "~> 0.4.0") 27 | end 28 | 29 | -------------------------------------------------------------------------------- /src/NetworkAccessManager.cpp: -------------------------------------------------------------------------------- 1 | #include "NetworkAccessManager.h" 2 | #include "WebPage.h" 3 | #include 4 | 5 | 6 | NetworkAccessManager::NetworkAccessManager(QObject *parent):QNetworkAccessManager(parent) { 7 | } 8 | 9 | QNetworkReply* NetworkAccessManager::createRequest(QNetworkAccessManager::Operation operation, const QNetworkRequest &request, QIODevice * outgoingData = 0) { 10 | QNetworkRequest new_request(request); 11 | if (operation != QNetworkAccessManager::PostOperation && operation != QNetworkAccessManager::PutOperation) { 12 | new_request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant()); 13 | } 14 | QHashIterator item(m_headers); 15 | while (item.hasNext()) { 16 | item.next(); 17 | new_request.setRawHeader(item.key().toAscii(), item.value().toAscii()); 18 | } 19 | return QNetworkAccessManager::createRequest(operation, new_request, outgoingData); 20 | }; 21 | 22 | void NetworkAccessManager::addHeader(QString key, QString value) { 23 | m_headers.insert(key, value); 24 | }; 25 | 26 | void NetworkAccessManager::resetHeaders() { 27 | m_headers.clear(); 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /src/UnsupportedContentHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "UnsupportedContentHandler.h" 2 | #include "WebPage.h" 3 | #include 4 | 5 | UnsupportedContentHandler::UnsupportedContentHandler(WebPage *page, QNetworkReply *reply, QObject *parent) : QObject(parent) { 6 | m_page = page; 7 | m_reply = reply; 8 | connect(m_reply, SIGNAL(finished()), this, SLOT(handleUnsupportedContent())); 9 | disconnect(m_page, SIGNAL(loadFinished(bool)), m_page, SLOT(loadFinished(bool))); 10 | } 11 | 12 | void UnsupportedContentHandler::handleUnsupportedContent() { 13 | QVariant contentMimeType = m_reply->header(QNetworkRequest::ContentTypeHeader); 14 | if(contentMimeType.isNull()) { 15 | this->finish(false); 16 | } else { 17 | this->loadUnsupportedContent(); 18 | this->finish(true); 19 | } 20 | this->deleteLater(); 21 | } 22 | 23 | void UnsupportedContentHandler::loadUnsupportedContent() { 24 | QByteArray text = m_reply->readAll(); 25 | m_page->mainFrame()->setContent(text, QString("text/plain"), m_reply->url()); 26 | } 27 | 28 | void UnsupportedContentHandler::finish(bool success) { 29 | connect(m_page, SIGNAL(loadFinished(bool)), m_page, SLOT(loadFinished(bool))); 30 | m_page->replyFinished(m_reply); 31 | m_page->loadFinished(success); 32 | } 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/autorun' 3 | require 'rbconfig' 4 | 5 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')).freeze 6 | 7 | $LOAD_PATH << File.join(PROJECT_ROOT, 'lib') 8 | 9 | Dir[File.join(PROJECT_ROOT, 'spec', 'support', '**', '*.rb')].each { |file| require(file) } 10 | 11 | spec_dir = nil 12 | $:.detect do |dir| 13 | if File.exists? File.join(dir, "capybara.rb") 14 | spec_dir = File.expand_path(File.join(dir,"..","spec")) 15 | $:.unshift( spec_dir ) 16 | end 17 | end 18 | 19 | RSpec.configure do |c| 20 | c.filter_run_excluding :skip_on_windows => !(RbConfig::CONFIG['host_os'] =~ /mingw32/).nil? 21 | end 22 | 23 | require File.join(spec_dir, "spec_helper") 24 | 25 | require 'capybara/driver/webkit/connection' 26 | require 'capybara/driver/webkit/browser' 27 | connection = Capybara::Driver::Webkit::Connection.new(:socket_class => TCPSocket, :stdout => nil) 28 | $webkit_browser = Capybara::Driver::Webkit::Browser.new(connection) 29 | 30 | Capybara.register_driver :reusable_webkit do |app| 31 | Capybara::Driver::Webkit.new(app, :browser => $webkit_browser) 32 | end 33 | 34 | def with_env_vars(vars) 35 | old_env_variables = {} 36 | vars.each do |key, value| 37 | old_env_variables[key] = ENV[key] 38 | ENV[key] = value 39 | end 40 | 41 | yield 42 | 43 | old_env_variables.each do |key, value| 44 | ENV[key] = value 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/cookie_jar.rb: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | 3 | # A simple cookie jar implementation. 4 | # Does not take special cookie attributes 5 | # into account like expire, max-age, httponly, secure 6 | class Capybara::Driver::Webkit::CookieJar 7 | attr_reader :browser 8 | 9 | def initialize(browser) 10 | @browser = browser 11 | end 12 | 13 | def [](*args) 14 | cookie = find(*args) 15 | cookie && cookie.value 16 | end 17 | 18 | def find(name, domain = nil, path = "/") 19 | # we are sorting by path size because more specific paths take 20 | # precendence 21 | cookies.sort_by { |c| -c.path.size }.find { |c| 22 | c.name.downcase == name.downcase && 23 | (!domain || valid_domain?(c, domain)) && 24 | (!path || valid_path?(c, path)) 25 | } 26 | end 27 | 28 | protected 29 | 30 | def valid_domain?(cookie, domain) 31 | ends_with?(("." + domain).downcase, 32 | normalize_domain(cookie.domain).downcase) 33 | end 34 | 35 | def normalize_domain(domain) 36 | domain = "." + domain unless domain[0,1] == "." 37 | domain 38 | end 39 | 40 | def valid_path?(cookie, path) 41 | starts_with?(path, cookie.path) 42 | end 43 | 44 | def ends_with?(str, suffix) 45 | str[-suffix.size..-1] == suffix 46 | end 47 | 48 | def starts_with?(str, prefix) 49 | str[0, prefix.size] == prefix 50 | end 51 | 52 | def cookies 53 | browser.get_cookies.map { |c| WEBrick::Cookie.parse_set_cookie(c) } 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | capybara-webkit (0.11.0) 5 | capybara (>= 1.0.0, < 1.2) 6 | json 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | appraisal (0.4.0) 12 | bundler 13 | rake 14 | capybara (1.1.2) 15 | mime-types (>= 1.16) 16 | nokogiri (>= 1.3.3) 17 | rack (>= 1.0.0) 18 | rack-test (>= 0.5.4) 19 | selenium-webdriver (~> 2.0) 20 | xpath (~> 0.1.4) 21 | childprocess (0.3.1) 22 | ffi (~> 1.0.6) 23 | diff-lcs (1.1.2) 24 | ffi (1.0.11) 25 | json (1.6.5) 26 | mime-types (1.17.2) 27 | mini_magick (3.2.1) 28 | subexec (~> 0.0.4) 29 | multi_json (1.0.4) 30 | nokogiri (1.5.0) 31 | rack (1.3.2) 32 | rack-test (0.6.1) 33 | rack (>= 1.0) 34 | rake (0.9.2) 35 | rspec (2.6.0) 36 | rspec-core (~> 2.6.0) 37 | rspec-expectations (~> 2.6.0) 38 | rspec-mocks (~> 2.6.0) 39 | rspec-core (2.6.4) 40 | rspec-expectations (2.6.0) 41 | diff-lcs (~> 1.1.2) 42 | rspec-mocks (2.6.0) 43 | rubyzip (0.9.6.1) 44 | selenium-webdriver (2.19.0) 45 | childprocess (>= 0.2.5) 46 | ffi (~> 1.0.9) 47 | multi_json (~> 1.0.4) 48 | rubyzip 49 | sinatra (1.1.2) 50 | rack (~> 1.1) 51 | tilt (~> 1.2) 52 | subexec (0.0.4) 53 | tilt (1.2.2) 54 | xpath (0.1.4) 55 | nokogiri (~> 1.3) 56 | 57 | PLATFORMS 58 | ruby 59 | x86-mingw32 60 | 61 | DEPENDENCIES 62 | appraisal (~> 0.4.0) 63 | capybara-webkit! 64 | mini_magick 65 | rake 66 | rspec (~> 2.6.0) 67 | sinatra 68 | -------------------------------------------------------------------------------- /src/PageLoadingCommand.cpp: -------------------------------------------------------------------------------- 1 | #include "PageLoadingCommand.h" 2 | #include "Command.h" 3 | #include "WebPage.h" 4 | 5 | PageLoadingCommand::PageLoadingCommand(Command *command, WebPage *page, QObject *parent) : QObject(parent) { 6 | m_page = page; 7 | m_command = command; 8 | m_pageLoadingFromCommand = false; 9 | m_pageSuccess = true; 10 | m_pendingResponse = NULL; 11 | connect(m_page, SIGNAL(loadStarted()), this, SLOT(pageLoadingFromCommand())); 12 | connect(m_page, SIGNAL(pageFinished(bool)), this, SLOT(pendingLoadFinished(bool))); 13 | } 14 | 15 | void PageLoadingCommand::start() { 16 | connect(m_command, SIGNAL(finished(Response *)), this, SLOT(commandFinished(Response *))); 17 | m_command->start(); 18 | }; 19 | 20 | void PageLoadingCommand::pendingLoadFinished(bool success) { 21 | m_pageSuccess = success; 22 | if (m_pageLoadingFromCommand) { 23 | m_pageLoadingFromCommand = false; 24 | if (m_pendingResponse) { 25 | if (m_pageSuccess) { 26 | emit finished(m_pendingResponse); 27 | } else { 28 | QString message = m_page->failureString(); 29 | emit finished(new Response(false, message)); 30 | } 31 | } 32 | } 33 | } 34 | 35 | void PageLoadingCommand::pageLoadingFromCommand() { 36 | m_pageLoadingFromCommand = true; 37 | } 38 | 39 | void PageLoadingCommand::commandFinished(Response *response) { 40 | disconnect(m_page, SIGNAL(loadStarted()), this, SLOT(pageLoadingFromCommand())); 41 | m_command->deleteLater(); 42 | if (m_pageLoadingFromCommand) 43 | m_pendingResponse = response; 44 | else 45 | emit finished(response); 46 | } 47 | -------------------------------------------------------------------------------- /lib/capybara_webkit_builder.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "rbconfig" 3 | 4 | module CapybaraWebkitBuilder 5 | extend self 6 | 7 | def make_bin 8 | ENV['MAKE'] || 'make' 9 | end 10 | 11 | def qmake_bin 12 | ENV['QMAKE'] || 'qmake' 13 | end 14 | 15 | def spec 16 | ENV['SPEC'] || os_spec 17 | end 18 | 19 | def os_spec 20 | case RbConfig::CONFIG['host_os'] 21 | when /linux/ 22 | "linux-g++" 23 | when /freebsd/ 24 | "freebsd-g++" 25 | when /mingw32/ 26 | "win32-g++" 27 | else 28 | "macx-g++" 29 | end 30 | end 31 | 32 | def sh(command) 33 | system(command) 34 | success = $?.exitstatus == 0 35 | if $?.exitstatus == 127 36 | puts "Command '#{command}' not available" 37 | elsif !success 38 | puts "Command '#{command}' failed" 39 | end 40 | success 41 | end 42 | 43 | def makefile 44 | sh("#{make_env_variables} #{qmake_bin} -spec #{spec}") 45 | end 46 | 47 | def qmake 48 | sh("#{make_env_variables} #{make_bin} qmake") 49 | end 50 | 51 | def make_env_variables 52 | case RbConfig::CONFIG['host_os'] 53 | when /mingw32/ 54 | '' 55 | else 56 | "LANG='en_US.UTF-8'" 57 | end 58 | end 59 | 60 | def path_to_binary 61 | case RUBY_PLATFORM 62 | when /mingw32/ 63 | "src/debug/webkit_server.exe" 64 | else 65 | "src/webkit_server" 66 | end 67 | end 68 | 69 | def build 70 | sh(make_bin) or return false 71 | 72 | FileUtils.mkdir("bin") unless File.directory?("bin") 73 | FileUtils.cp(path_to_binary, "bin", :preserve => true) 74 | end 75 | 76 | def build_all 77 | makefile && 78 | qmake && 79 | build 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/cookie_jar_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit/cookie_jar' 3 | 4 | describe Capybara::Driver::Webkit::CookieJar do 5 | let(:browser) { 6 | browser = double("Browser") 7 | browser.stub(:get_cookies) { [ 8 | "cookie1=1; domain=.example.org; path=/", 9 | "cookie1=2; domain=.example.org; path=/dir1/", 10 | "cookie1=3; domain=.facebook.com; path=/", 11 | "cookie2=4; domain=.sub1.example.org; path=/", 12 | ] } 13 | browser 14 | } 15 | 16 | subject { Capybara::Driver::Webkit::CookieJar.new(browser) } 17 | 18 | describe "#find" do 19 | it "returns a cookie object" do 20 | subject.find("cookie1", "www.facebook.com").domain.should == ".facebook.com" 21 | end 22 | 23 | it "returns the right cookie for every given domain/path" do 24 | subject.find("cookie1", "example.org").value.should == "1" 25 | subject.find("cookie1", "www.facebook.com").value.should == "3" 26 | subject.find("cookie2", "sub1.example.org").value.should == "4" 27 | end 28 | 29 | it "does not return a cookie from other domain" do 30 | subject.find("cookie2", "www.example.org").should == nil 31 | end 32 | 33 | it "respects path precedence rules" do 34 | subject.find("cookie1", "www.example.org").value.should == "1" 35 | subject.find("cookie1", "www.example.org", "/dir1/123").value.should == "2" 36 | end 37 | end 38 | 39 | describe "#[]" do 40 | it "returns the first matching cookie's value" do 41 | subject["cookie1", "example.org"].should == "1" 42 | end 43 | 44 | it "returns nil if no cookie is found" do 45 | subject["notexisting"].should == nil 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/FrameFocus.cpp: -------------------------------------------------------------------------------- 1 | #include "FrameFocus.h" 2 | #include "Command.h" 3 | #include "WebPage.h" 4 | 5 | FrameFocus::FrameFocus(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | } 7 | 8 | void FrameFocus::start() { 9 | findFrames(); 10 | switch(arguments().length()) { 11 | case 1: 12 | focusId(arguments()[0]); 13 | break; 14 | case 2: 15 | focusIndex(arguments()[1].toInt()); 16 | break; 17 | default: 18 | focusParent(); 19 | } 20 | } 21 | 22 | void FrameFocus::findFrames() { 23 | frames = page()->currentFrame()->childFrames(); 24 | } 25 | 26 | void FrameFocus::focusIndex(int index) { 27 | if (isFrameAtIndex(index)) { 28 | frames[index]->setFocus(); 29 | success(); 30 | } else { 31 | frameNotFound(); 32 | } 33 | } 34 | 35 | bool FrameFocus::isFrameAtIndex(int index) { 36 | return 0 <= index && index < frames.length(); 37 | } 38 | 39 | void FrameFocus::focusId(QString name) { 40 | for (int i = 0; i < frames.length(); i++) { 41 | if (frames[i]->frameName().compare(name) == 0) { 42 | frames[i]->setFocus(); 43 | success(); 44 | return; 45 | } 46 | } 47 | 48 | frameNotFound(); 49 | } 50 | 51 | void FrameFocus::focusParent() { 52 | if (page()->currentFrame()->parentFrame() == 0) { 53 | emit finished(new Response(false, QString("Already at parent frame."))); 54 | } else { 55 | page()->currentFrame()->parentFrame()->setFocus(); 56 | success(); 57 | } 58 | } 59 | 60 | void FrameFocus::frameNotFound() { 61 | emit finished(new Response(false, QString("Unable to locate frame. "))); 62 | } 63 | 64 | void FrameFocus::success() { 65 | emit finished(new Response(true)); 66 | } 67 | -------------------------------------------------------------------------------- /gemfiles/1.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /Users/jferris/Source/capybara-webkit 3 | specs: 4 | capybara-webkit (0.8.0) 5 | capybara (>= 1.0.0, < 1.2) 6 | json 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | appraisal (0.4.0) 12 | bundler 13 | rake 14 | capybara (1.0.1) 15 | mime-types (>= 1.16) 16 | nokogiri (>= 1.3.3) 17 | rack (>= 1.0.0) 18 | rack-test (>= 0.5.4) 19 | selenium-webdriver (~> 2.0) 20 | xpath (~> 0.1.4) 21 | childprocess (0.2.2) 22 | ffi (~> 1.0.6) 23 | diff-lcs (1.1.3) 24 | ffi (1.0.10) 25 | json (1.6.3) 26 | json_pure (1.6.1) 27 | mime-types (1.17.2) 28 | mini_magick (3.3) 29 | subexec (~> 0.1.0) 30 | nokogiri (1.5.0) 31 | rack (1.3.5) 32 | rack-protection (1.1.4) 33 | rack 34 | rack-test (0.6.1) 35 | rack (>= 1.0) 36 | rake (0.9.2.2) 37 | rspec (2.6.0) 38 | rspec-core (~> 2.6.0) 39 | rspec-expectations (~> 2.6.0) 40 | rspec-mocks (~> 2.6.0) 41 | rspec-core (2.6.4) 42 | rspec-expectations (2.6.0) 43 | diff-lcs (~> 1.1.2) 44 | rspec-mocks (2.6.0) 45 | rubyzip (0.9.4) 46 | selenium-webdriver (2.12.1) 47 | childprocess (>= 0.2.1) 48 | ffi (~> 1.0.9) 49 | json_pure 50 | rubyzip 51 | sinatra (1.3.1) 52 | rack (~> 1.3, >= 1.3.4) 53 | rack-protection (~> 1.1, >= 1.1.2) 54 | tilt (~> 1.3, >= 1.3.3) 55 | subexec (0.1.0) 56 | tilt (1.3.3) 57 | xpath (0.1.4) 58 | nokogiri (~> 1.3) 59 | 60 | PLATFORMS 61 | ruby 62 | 63 | DEPENDENCIES 64 | appraisal (~> 0.4.0) 65 | capybara (~> 1.0.0) 66 | capybara-webkit! 67 | mini_magick 68 | rake 69 | rspec (~> 2.6.0) 70 | sinatra 71 | -------------------------------------------------------------------------------- /gemfiles/1.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /Users/jferris/Source/capybara-webkit 3 | specs: 4 | capybara-webkit (0.8.0) 5 | capybara (>= 1.0.0, < 1.2) 6 | json 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | appraisal (0.4.0) 12 | bundler 13 | rake 14 | capybara (1.1.1) 15 | mime-types (>= 1.16) 16 | nokogiri (>= 1.3.3) 17 | rack (>= 1.0.0) 18 | rack-test (>= 0.5.4) 19 | selenium-webdriver (~> 2.0) 20 | xpath (~> 0.1.4) 21 | childprocess (0.2.2) 22 | ffi (~> 1.0.6) 23 | diff-lcs (1.1.3) 24 | ffi (1.0.10) 25 | json (1.6.3) 26 | json_pure (1.6.1) 27 | mime-types (1.17.2) 28 | mini_magick (3.3) 29 | subexec (~> 0.1.0) 30 | nokogiri (1.5.0) 31 | rack (1.3.5) 32 | rack-protection (1.1.4) 33 | rack 34 | rack-test (0.6.1) 35 | rack (>= 1.0) 36 | rake (0.9.2.2) 37 | rspec (2.6.0) 38 | rspec-core (~> 2.6.0) 39 | rspec-expectations (~> 2.6.0) 40 | rspec-mocks (~> 2.6.0) 41 | rspec-core (2.6.4) 42 | rspec-expectations (2.6.0) 43 | diff-lcs (~> 1.1.2) 44 | rspec-mocks (2.6.0) 45 | rubyzip (0.9.4) 46 | selenium-webdriver (2.12.1) 47 | childprocess (>= 0.2.1) 48 | ffi (~> 1.0.9) 49 | json_pure 50 | rubyzip 51 | sinatra (1.3.1) 52 | rack (~> 1.3, >= 1.3.4) 53 | rack-protection (~> 1.1, >= 1.1.2) 54 | tilt (~> 1.3, >= 1.3.3) 55 | subexec (0.1.0) 56 | tilt (1.3.3) 57 | xpath (0.1.4) 58 | nokogiri (~> 1.3) 59 | 60 | PLATFORMS 61 | ruby 62 | 63 | DEPENDENCIES 64 | appraisal (~> 0.4.0) 65 | capybara (~> 1.1.0) 66 | capybara-webkit! 67 | mini_magick 68 | rake 69 | rspec (~> 2.6.0) 70 | sinatra 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests. Here's a quick guide: 2 | 3 | Dependencies 4 | 5 | Some of the tests depend on the `identify` command that comes with Imagemagick. 6 | Imagemagick can be installed via [homebrew](http://mxcl.github.com/homebrew/). 7 | 8 | brew install imagemagick 9 | 10 | Contributing 11 | 12 | 1. Fork the repo. 13 | 14 | 2. Run the tests. We only take pull requests with passing tests, and it's great 15 | to know that you have a clean slate: `bundle && bundle exec rake` 16 | 17 | 3. Add a test for your change. Only refactoring and documentation changes 18 | require no new tests. If you are adding functionality or fixing a bug, we need 19 | a test! 20 | 21 | 4. Make the test pass. 22 | 23 | 5. Push to your fork and submit a pull request. 24 | 25 | 26 | At this point you're waiting on us. We like to at least comment on, if not 27 | accept, pull requests within three business days (and, typically, one business 28 | day). We may suggest some changes or improvements or alternatives. 29 | 30 | Some things that will increase the chance that your pull request is accepted, 31 | taken straight from the Ruby on Rails guide: 32 | 33 | * Use Rails idioms and helpers 34 | * Include tests that fail without your code, and pass with it 35 | * Update the documentation, the surrounding one, examples elsewhere, guides, 36 | whatever is affected by your contribution 37 | 38 | Syntax: 39 | 40 | * Two spaces, no tabs. 41 | * No trailing whitespace. Blank lines should not have any space. 42 | * Prefer &&/|| over and/or. 43 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 44 | * a = b and not a=b. 45 | * Follow the conventions you see used in the source already. 46 | 47 | And in case we didn't emphasize it enough: we love tests! 48 | -------------------------------------------------------------------------------- /spec/driver_resize_window_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit' 3 | 4 | describe Capybara::Driver::Webkit, "#resize_window(width, height)" do 5 | 6 | before(:all) do 7 | app = lambda do |env| 8 | body = <<-HTML 9 | 10 | 11 |

UNKNOWN

12 | 13 | 18 | 19 | 20 | 21 | HTML 22 | 23 | [ 24 | 200, 25 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 26 | [body] 27 | ] 28 | end 29 | 30 | @driver = Capybara::Driver::Webkit.new(app, :browser => $webkit_browser) 31 | end 32 | 33 | DEFAULT_DIMENTIONS = "[1680x1050]" 34 | 35 | it "resizes the window to the specified size" do 36 | @driver.visit("/") 37 | 38 | @driver.resize_window(800, 600) 39 | @driver.body.should include("[800x600]") 40 | 41 | @driver.resize_window(300, 100) 42 | @driver.body.should include("[300x100]") 43 | end 44 | 45 | it "resizes the window to the specified size even before the document has loaded" do 46 | @driver.resize_window(800, 600) 47 | @driver.visit("/") 48 | @driver.body.should include("[800x600]") 49 | end 50 | 51 | it "resets the window to the default size when the driver is reset" do 52 | @driver.resize_window(800, 600) 53 | @driver.reset! 54 | @driver.visit("/") 55 | @driver.body.should include(DEFAULT_DIMENTIONS) 56 | end 57 | 58 | after(:all) { @driver.reset! } 59 | end 60 | -------------------------------------------------------------------------------- /src/webkit_server.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | TARGET = webkit_server 3 | DESTDIR = . 4 | HEADERS = \ 5 | IgnoreSslErrors.h \ 6 | ResizeWindow.h \ 7 | CurrentUrl.h \ 8 | RequestedUrl.h \ 9 | ConsoleMessages.h \ 10 | WebPage.h \ 11 | Server.h \ 12 | Connection.h \ 13 | Command.h \ 14 | Visit.h \ 15 | Find.h \ 16 | Reset.h \ 17 | Node.h \ 18 | JavascriptInvocation.h \ 19 | Url.h \ 20 | Source.h \ 21 | Evaluate.h \ 22 | Execute.h \ 23 | FrameFocus.h \ 24 | Response.h \ 25 | NetworkAccessManager.h \ 26 | NetworkCookieJar.h \ 27 | Header.h \ 28 | Render.h \ 29 | body.h \ 30 | Status.h \ 31 | Headers.h \ 32 | UnsupportedContentHandler.h \ 33 | SetCookie.h \ 34 | ClearCookies.h \ 35 | GetCookies.h \ 36 | CommandParser.h \ 37 | CommandFactory.h \ 38 | SetProxy.h \ 39 | NullCommand.h \ 40 | PageLoadingCommand.h \ 41 | 42 | SOURCES = \ 43 | IgnoreSslErrors.cpp \ 44 | ResizeWindow.cpp \ 45 | CurrentUrl.cpp \ 46 | RequestedUrl.cpp \ 47 | ConsoleMessages.cpp \ 48 | main.cpp \ 49 | WebPage.cpp \ 50 | Server.cpp \ 51 | Connection.cpp \ 52 | Command.cpp \ 53 | Visit.cpp \ 54 | Find.cpp \ 55 | Reset.cpp \ 56 | Node.cpp \ 57 | JavascriptInvocation.cpp \ 58 | Url.cpp \ 59 | Source.cpp \ 60 | Evaluate.cpp \ 61 | Execute.cpp \ 62 | FrameFocus.cpp \ 63 | Response.cpp \ 64 | NetworkAccessManager.cpp \ 65 | NetworkCookieJar.cpp \ 66 | Header.cpp \ 67 | Render.cpp \ 68 | body.cpp \ 69 | Status.cpp \ 70 | Headers.cpp \ 71 | UnsupportedContentHandler.cpp \ 72 | SetCookie.cpp \ 73 | ClearCookies.cpp \ 74 | GetCookies.cpp \ 75 | CommandParser.cpp \ 76 | CommandFactory.cpp \ 77 | SetProxy.cpp \ 78 | NullCommand.cpp \ 79 | PageLoadingCommand.cpp \ 80 | 81 | RESOURCES = webkit_server.qrc 82 | QT += network webkit 83 | CONFIG += console 84 | CONFIG -= app_bundle 85 | 86 | -------------------------------------------------------------------------------- /spec/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit/connection' 3 | 4 | describe Capybara::Driver::Webkit::Connection do 5 | it "boots a server to talk to" do 6 | url = @rack_server.url("/") 7 | connection.puts "Visit" 8 | connection.puts 1 9 | connection.puts url.to_s.bytesize 10 | connection.print url 11 | connection.gets.should == "ok\n" 12 | connection.gets.should == "0\n" 13 | connection.puts "Body" 14 | connection.puts 0 15 | connection.gets.should == "ok\n" 16 | response_length = connection.gets.to_i 17 | response = connection.read(response_length) 18 | response.should include("Hey there") 19 | end 20 | 21 | it 'forwards stdout to the given IO object' do 22 | io = StringIO.new 23 | redirected_connection = Capybara::Driver::Webkit::Connection.new(:stdout => io) 24 | script = 'console.log("hello world")' 25 | redirected_connection.puts "Execute" 26 | redirected_connection.puts 1 27 | redirected_connection.puts script.to_s.bytesize 28 | redirected_connection.print script 29 | sleep(0.5) 30 | io.string.should include "hello world\n" 31 | end 32 | 33 | it "returns the server port" do 34 | connection.port.should be_between 0x400, 0xffff 35 | end 36 | 37 | it "chooses a new port number for a new connection" do 38 | new_connection = Capybara::Driver::Webkit::Connection.new 39 | new_connection.port.should_not == connection.port 40 | end 41 | 42 | let(:connection) { Capybara::Driver::Webkit::Connection.new } 43 | 44 | before(:all) do 45 | @app = lambda do |env| 46 | body = "Hey there" 47 | [200, 48 | { 'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s }, 49 | [body]] 50 | end 51 | @rack_server = Capybara::Server.new(@app) 52 | @rack_server.boot 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rspec/core/rake_task' 3 | require 'capybara_webkit_builder' 4 | require 'appraisal' 5 | 6 | Bundler::GemHelper.install_tasks 7 | 8 | desc "Generate a Makefile using qmake" 9 | file 'Makefile' do 10 | CapybaraWebkitBuilder.makefile or exit(1) 11 | end 12 | 13 | desc "Regenerate dependencies using qmake" 14 | task :qmake => 'Makefile' do 15 | CapybaraWebkitBuilder.qmake or exit(1) 16 | end 17 | 18 | desc "Build the webkit server" 19 | task :build => :qmake do 20 | CapybaraWebkitBuilder.build or exit(1) 21 | end 22 | 23 | file 'bin/webkit_server' => :build 24 | 25 | RSpec::Core::RakeTask.new do |t| 26 | t.pattern = "spec/**/*_spec.rb" 27 | t.rspec_opts = "--format progress" 28 | end 29 | 30 | desc "Default: build and run all specs" 31 | task :default => [:build, :spec] 32 | 33 | desc "Generate a new command called NAME" 34 | task :generate_command do 35 | name = ENV['NAME'] or raise "Provide a name with NAME=" 36 | 37 | header = "src/#{name}.h" 38 | source = "src/#{name}.cpp" 39 | 40 | %w(h cpp).each do |extension| 41 | File.open("templates/Command.#{extension}", "r") do |source_file| 42 | contents = source_file.read 43 | contents.gsub!("NAME", name) 44 | File.open("src/#{name}.#{extension}", "w") do |target_file| 45 | target_file.write(contents) 46 | end 47 | end 48 | end 49 | 50 | Dir.glob("src/*.pro").each do |project_file_name| 51 | project = IO.read(project_file_name) 52 | project.gsub!(/^(HEADERS = .*)/, "\\1\n #{name}.h \\") 53 | project.gsub!(/^(SOURCES = .*)/, "\\1\n #{name}.cpp \\") 54 | File.open(project_file_name, "w") { |file| file.write(project) } 55 | end 56 | 57 | File.open("src/find_command.h", "a") do |file| 58 | file.write("CHECK_COMMAND(#{name})\n") 59 | end 60 | 61 | command_factory_file_name = "src/CommandFactory.cpp" 62 | command_factory = IO.read(command_factory_file_name) 63 | command_factory.sub!(/^$/, "#include \"#{name}.h\"\n") 64 | File.open(command_factory_file_name, "w") { |file| file.write(command_factory) } 65 | end 66 | -------------------------------------------------------------------------------- /src/CommandParser.cpp: -------------------------------------------------------------------------------- 1 | #include "CommandParser.h" 2 | #include "CommandFactory.h" 3 | #include "Command.h" 4 | 5 | #include 6 | 7 | CommandParser::CommandParser(QIODevice *device, CommandFactory *commandFactory, QObject *parent) : 8 | QObject(parent) { 9 | m_device = device; 10 | m_expectingDataSize = -1; 11 | m_commandFactory = commandFactory; 12 | connect(m_device, SIGNAL(readyRead()), this, SLOT(checkNext())); 13 | } 14 | 15 | void CommandParser::checkNext() { 16 | if (m_expectingDataSize == -1) { 17 | if (m_device->canReadLine()) { 18 | readLine(); 19 | checkNext(); 20 | } 21 | } else { 22 | if (m_device->bytesAvailable() >= m_expectingDataSize) { 23 | readDataBlock(); 24 | checkNext(); 25 | } 26 | } 27 | } 28 | 29 | void CommandParser::readLine() { 30 | char buffer[128]; 31 | qint64 lineLength = m_device->readLine(buffer, 128); 32 | if (lineLength != -1) { 33 | buffer[lineLength - 1] = 0; 34 | processNext(buffer); 35 | } 36 | } 37 | 38 | void CommandParser::readDataBlock() { 39 | char *buffer = new char[m_expectingDataSize + 1]; 40 | m_device->read(buffer, m_expectingDataSize); 41 | buffer[m_expectingDataSize] = 0; 42 | processNext(buffer); 43 | m_expectingDataSize = -1; 44 | delete[] buffer; 45 | } 46 | 47 | void CommandParser::processNext(const char *data) { 48 | if (m_commandName.isNull()) { 49 | m_commandName = data; 50 | m_argumentsExpected = -1; 51 | } else { 52 | processArgument(data); 53 | } 54 | } 55 | 56 | void CommandParser::processArgument(const char *data) { 57 | if (m_argumentsExpected == -1) { 58 | m_argumentsExpected = QString(data).toInt(); 59 | } else if (m_expectingDataSize == -1) { 60 | m_expectingDataSize = QString(data).toInt(); 61 | } else { 62 | m_arguments.append(QString::fromUtf8(data)); 63 | } 64 | 65 | if (m_arguments.length() == m_argumentsExpected) { 66 | Command *command = m_commandFactory->createCommand(m_commandName.toAscii().constData(), m_arguments); 67 | emit commandReady(command); 68 | reset(); 69 | } 70 | } 71 | 72 | void CommandParser::reset() { 73 | m_commandName = QString(); 74 | m_arguments.clear(); 75 | m_argumentsExpected = -1; 76 | } 77 | -------------------------------------------------------------------------------- /src/WebPage.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | class WebPage : public QWebPage { 4 | Q_OBJECT 5 | 6 | public: 7 | WebPage(QObject *parent = 0); 8 | QVariant invokeCapybaraFunction(const char *name, QStringList &arguments); 9 | QVariant invokeCapybaraFunction(QString &name, QStringList &arguments); 10 | QString failureString(); 11 | QString userAgentForUrl(const QUrl &url ) const; 12 | void setUserAgent(QString userAgent); 13 | int getLastStatus(); 14 | void resetResponseHeaders(); 15 | void setCustomNetworkAccessManager(); 16 | bool render(const QString &fileName); 17 | virtual bool extension (Extension extension, const ExtensionOption *option=0, ExtensionReturn *output=0); 18 | void ignoreSslErrors(); 19 | QString consoleMessages(); 20 | void resetConsoleMessages(); 21 | void resetWindowSize(); 22 | 23 | public slots: 24 | bool shouldInterruptJavaScript(); 25 | void injectJavascriptHelpers(); 26 | void loadStarted(); 27 | void loadFinished(bool); 28 | bool isLoading() const; 29 | QString pageHeaders(); 30 | void frameCreated(QWebFrame *); 31 | void replyFinished(QNetworkReply *reply); 32 | void handleSslErrorsForReply(QNetworkReply *reply, const QList &); 33 | void handleUnsupportedContent(QNetworkReply *reply); 34 | 35 | signals: 36 | void pageFinished(bool); 37 | 38 | protected: 39 | virtual void javaScriptConsoleMessage(const QString &message, int lineNumber, const QString &sourceID); 40 | virtual void javaScriptAlert(QWebFrame *frame, const QString &message); 41 | virtual bool javaScriptConfirm(QWebFrame *frame, const QString &message); 42 | virtual bool javaScriptPrompt(QWebFrame *frame, const QString &message, const QString &defaultValue, QString *result); 43 | virtual QString chooseFile(QWebFrame * parentFrame, const QString &suggestedFile); 44 | 45 | private: 46 | QString m_capybaraJavascript; 47 | QString m_userAgent; 48 | bool m_loading; 49 | QString getLastAttachedFileName(); 50 | void loadJavascript(); 51 | void setUserStylesheet(); 52 | int m_lastStatus; 53 | QString m_pageHeaders; 54 | bool m_ignoreSslErrors; 55 | QStringList m_consoleMessages; 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /spec/self_signed_ssl_cert.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | pem = <<-PEM_ENCODED 4 | -----BEGIN PRIVATE KEY----- 5 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKW+grT6YW3gv79y 6 | P9JkQDtm3cDSUhAhd/TEyRBt/pSKz3pNSygsleBJl2d7g8k0fteec95a7YnYRKGH 7 | XhIpUOvl/3uaV2NVipqxwB+Z+0M+7HegxL3e4unaRFy9kf9/UXJzmuA9BTMLrm/w 8 | IoW17f+dz7BIFZhCvRurkrmvzraNAgMBAAECgYBv4uB3bYJx20N16Jk+3OAjeXh/ 9 | Hzu4me9Rc7pLdgVinyYaaK0wrJBsfSFRASdgnyh1RAjx9K3f3PfPlwMg/XUbA6Yd 10 | YOYlMnBUwCJX09TH8RFFCzJzbBylpk/sTF1geICln2O2BloT2cM24PlEPvyz1xLa 11 | XvxCOnJJfgNU1K6kvQJBANcEVyOMJ9RBfI8gj1o7S70J9yJRI4jvqxuvcOxJBCi6 12 | CDatkh/getHswsE3sLj25VhrNsi6UQcN8Bjm8Yjt8BsCQQDFVe0uCwobseprMOuW 13 | dPU4+saN1cFnIT5Gp0iwYRPinjUlkh6H/AuUtUulKFXVmxk1KElpp1E3bxpCDgCp 14 | oe53AkArO1Mt8Ys8kSIzQO+xy8RRsQRAoSHM8atsuJyy1YeBjM4D+GguApuPQ9Rw 15 | tvrQZcv9OCleuJ98FKBW0XB1AKpLAkEAmOR3bJofDdAuWTjA/4TEzo32MsRwIZBv 16 | KNzJg+bjOkzrzp1EzIVrD5/b6S20O1j9EeOR5as+UN3jEVS6DLQrBwJAe5OTrDiQ 17 | vKtUaAwquC4f4Ia05KwJw+vFrPVaOqgc4QLdxRwx4PfV/Uw3OOqMolpPAvpUi9JI 18 | LAwIaTtCvo18OQ== 19 | -----END PRIVATE KEY----- 20 | -----BEGIN CERTIFICATE----- 21 | MIICgDCCAemgAwIBAgIJANWcyeZB2ql1MA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV 22 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 23 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xMTA5MjgyMDIx 24 | MTFaFw0xMjA5MjcyMDIxMTFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l 25 | LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 26 | BAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApb6CtPph 27 | beC/v3I/0mRAO2bdwNJSECF39MTJEG3+lIrPek1LKCyV4EmXZ3uDyTR+155z3lrt 28 | idhEoYdeEilQ6+X/e5pXY1WKmrHAH5n7Qz7sd6DEvd7i6dpEXL2R/39RcnOa4D0F 29 | Mwuub/AihbXt/53PsEgVmEK9G6uSua/Oto0CAwEAAaNQME4wHQYDVR0OBBYEFBAm 30 | x19zpY8M8FEcMXB4WQeUhqIiMB8GA1UdIwQYMBaAFBAmx19zpY8M8FEcMXB4WQeU 31 | hqIiMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAcznCusWS5Ws77IUl 32 | b87vdfCPVphICyfGHGWhHp3BZ3WLOphauAMdOYIiJGtPExyWr4DYpzbvx0+Ljg7G 33 | 2FNaI0QRXqbR5ccpvwm6KELmU0XDhykNaXiXSdnvIanr3z/hZ5e03mXAywo+nGQj 34 | UYTVCb6g/uHVNzXq1NgHGuqogKY= 35 | -----END CERTIFICATE----- 36 | PEM_ENCODED 37 | 38 | key = OpenSSL::PKey::RSA.new(pem) 39 | cert = OpenSSL::X509::Certificate.new(pem) 40 | $openssl_self_signed_ctx = OpenSSL::SSL::SSLContext.new 41 | $openssl_self_signed_ctx.key = key 42 | $openssl_self_signed_ctx.cert = cert 43 | -------------------------------------------------------------------------------- /spec/driver_rendering_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit' 3 | require 'mini_magick' 4 | 5 | describe Capybara::Driver::Webkit, "rendering an image" do 6 | 7 | before(:all) do 8 | # Set up the tmp directory and file name 9 | tmp_dir = File.join(PROJECT_ROOT, 'tmp') 10 | FileUtils.mkdir_p tmp_dir 11 | @file_name = File.join(tmp_dir, 'render-test.png') 12 | 13 | app = lambda do |env| 14 | body = <<-HTML 15 | 16 | 17 |

Hello World

18 | 19 | 20 | HTML 21 | [200, 22 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 23 | [body]] 24 | end 25 | 26 | @driver = Capybara::Driver::Webkit.new(app, :browser => $webkit_browser) 27 | @driver.visit("/hello/world?success=true") 28 | end 29 | 30 | after(:all) { @driver.reset! } 31 | 32 | def render(options) 33 | FileUtils.rm_f @file_name 34 | @driver.render @file_name, options 35 | 36 | @image = MiniMagick::Image.open @file_name 37 | end 38 | 39 | context "with default options" do 40 | before(:all){ render({}) } 41 | 42 | it "should be a PNG" do 43 | @image[:format].should == "PNG" 44 | end 45 | 46 | it "width default to 1000px (with 15px less for the scrollbar)" do 47 | @image[:width].should be < 1001 48 | @image[:width].should be > 1000-17 49 | end 50 | 51 | it "height should be at least 10px" do 52 | @image[:height].should >= 10 53 | end 54 | end 55 | 56 | context "with dimensions set larger than necessary" do 57 | before(:all){ render(:width => 500, :height => 400) } 58 | 59 | it "width should match the width given" do 60 | @image[:width].should == 500 61 | end 62 | 63 | it "height should match the height given" do 64 | @image[:height].should > 10 65 | end 66 | end 67 | 68 | context "with dimensions set smaller than the document's default" do 69 | before(:all){ render(:width => 50, :height => 10) } 70 | 71 | it "width should be greater than the width given" do 72 | @image[:width].should > 50 73 | end 74 | 75 | it "height should be greater than the height given" do 76 | @image[:height].should > 10 77 | end 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /src/Connection.cpp: -------------------------------------------------------------------------------- 1 | #include "Connection.h" 2 | #include "WebPage.h" 3 | #include "CommandParser.h" 4 | #include "CommandFactory.h" 5 | #include "PageLoadingCommand.h" 6 | #include "Command.h" 7 | 8 | #include 9 | 10 | Connection::Connection(QTcpSocket *socket, WebPage *page, QObject *parent) : 11 | QObject(parent) { 12 | m_socket = socket; 13 | m_page = page; 14 | m_commandFactory = new CommandFactory(page, this); 15 | m_commandParser = new CommandParser(socket, m_commandFactory, this); 16 | m_pageSuccess = true; 17 | m_commandWaiting = false; 18 | connect(m_socket, SIGNAL(readyRead()), m_commandParser, SLOT(checkNext())); 19 | connect(m_commandParser, SIGNAL(commandReady(Command *)), this, SLOT(commandReady(Command *))); 20 | connect(m_page, SIGNAL(pageFinished(bool)), this, SLOT(pendingLoadFinished(bool))); 21 | } 22 | 23 | void Connection::commandReady(Command *command) { 24 | m_queuedCommand = command; 25 | if (m_page->isLoading()) 26 | m_commandWaiting = true; 27 | else 28 | startCommand(); 29 | } 30 | 31 | void Connection::startCommand() { 32 | m_commandWaiting = false; 33 | if (m_pageSuccess) { 34 | m_runningCommand = new PageLoadingCommand(m_queuedCommand, m_page, this); 35 | connect(m_runningCommand, SIGNAL(finished(Response *)), this, SLOT(finishCommand(Response *))); 36 | m_runningCommand->start(); 37 | } else { 38 | writePageLoadFailure(); 39 | } 40 | } 41 | 42 | void Connection::pendingLoadFinished(bool success) { 43 | m_pageSuccess = success; 44 | if (m_commandWaiting) 45 | startCommand(); 46 | } 47 | 48 | void Connection::writePageLoadFailure() { 49 | m_pageSuccess = true; 50 | QString message = m_page->failureString(); 51 | writeResponse(new Response(false, message)); 52 | } 53 | 54 | void Connection::finishCommand(Response *response) { 55 | m_runningCommand->deleteLater(); 56 | writeResponse(response); 57 | } 58 | 59 | void Connection::writeResponse(Response *response) { 60 | if (response->isSuccess()) 61 | m_socket->write("ok\n"); 62 | else 63 | m_socket->write("failure\n"); 64 | 65 | QByteArray messageUtf8 = response->message(); 66 | QString messageLength = QString::number(messageUtf8.size()) + "\n"; 67 | m_socket->write(messageLength.toAscii()); 68 | m_socket->write(messageUtf8); 69 | delete response; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/Evaluate.cpp: -------------------------------------------------------------------------------- 1 | #include "Evaluate.h" 2 | #include "WebPage.h" 3 | #include 4 | 5 | Evaluate::Evaluate(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 6 | m_buffer = ""; 7 | } 8 | 9 | void Evaluate::start() { 10 | QVariant result = page()->currentFrame()->evaluateJavaScript(arguments()[0]); 11 | addVariant(result); 12 | emit finished(new Response(true, m_buffer)); 13 | } 14 | 15 | void Evaluate::addVariant(QVariant &object) { 16 | if (object.isValid()) { 17 | switch(object.type()) { 18 | case QMetaType::QString: 19 | { 20 | QString string = object.toString(); 21 | addString(string); 22 | } 23 | break; 24 | case QMetaType::QVariantList: 25 | { 26 | QVariantList list = object.toList(); 27 | addArray(list); 28 | } 29 | break; 30 | case QMetaType::Double: 31 | m_buffer.append(object.toString()); 32 | break; 33 | case QMetaType::QVariantMap: 34 | { 35 | QVariantMap map = object.toMap(); 36 | addMap(map); 37 | break; 38 | } 39 | case QMetaType::Bool: 40 | { 41 | m_buffer.append(object.toString()); 42 | break; 43 | } 44 | default: 45 | m_buffer.append("null"); 46 | } 47 | } else { 48 | m_buffer.append("null"); 49 | } 50 | } 51 | 52 | void Evaluate::addString(QString &string) { 53 | QString escapedString(string); 54 | escapedString.replace("\"", "\\\""); 55 | m_buffer.append("\""); 56 | m_buffer.append(escapedString); 57 | m_buffer.append("\""); 58 | } 59 | 60 | void Evaluate::addArray(QVariantList &list) { 61 | m_buffer.append("["); 62 | for (int i = 0; i < list.length(); i++) { 63 | if (i > 0) 64 | m_buffer.append(","); 65 | addVariant(list[i]); 66 | } 67 | m_buffer.append("]"); 68 | } 69 | 70 | void Evaluate::addMap(QVariantMap &map) { 71 | m_buffer.append("{"); 72 | QMapIterator iterator(map); 73 | while (iterator.hasNext()) { 74 | iterator.next(); 75 | QString key = iterator.key(); 76 | QVariant value = iterator.value(); 77 | addString(key); 78 | m_buffer.append(":"); 79 | addVariant(value); 80 | if (iterator.hasNext()) 81 | m_buffer.append(","); 82 | } 83 | m_buffer.append("}"); 84 | } 85 | -------------------------------------------------------------------------------- /src/CurrentUrl.cpp: -------------------------------------------------------------------------------- 1 | #include "CurrentUrl.h" 2 | #include "WebPage.h" 3 | 4 | CurrentUrl::CurrentUrl(WebPage *page, QStringList &arguments, QObject *parent) : Command(page, arguments, parent) { 5 | } 6 | 7 | /* 8 | * This CurrentUrl command attempts to produce a current_url value consistent 9 | * with that returned by the Selenium WebDriver Capybara driver. 10 | * 11 | * It does not currently return the correct value in the case of an iframe whose 12 | * source URL results in a redirect because the loading of the iframe does not 13 | * generate a history item. This is most likely a rare case and is consistent 14 | * with the current behavior of the capybara-webkit driver. 15 | * 16 | * The following two values are *not* affected by Javascript pushState. 17 | * 18 | * QWebFrame->url() 19 | * QWebHistoryItem.originalUrl() 20 | * 21 | * The following two values *are* affected by Javascript pushState. 22 | * 23 | * QWebFrame->requestedUrl() 24 | * QWebHistoryItem.url() 25 | * 26 | * In the cases that we have access to both the QWebFrame values and the 27 | * correct history item for that frame, we can compare the values and determine 28 | * if a redirect occurred and if pushState was used. The table below describes 29 | * the various combinations of URL values that are possible. 30 | * 31 | * O -> originally requested URL 32 | * R -> URL after redirection 33 | * P -> URL set by pushState 34 | * * -> denotes the desired URL value from the frame 35 | * 36 | * frame history 37 | * case url requestedUrl url originalUrl 38 | * ----------------------------------------------------------------- 39 | * regular load O O* O O 40 | * 41 | * redirect w/o R* O R O 42 | * pushState 43 | * 44 | * pushState O P* P O 45 | * only 46 | * 47 | * redirect w/ R P* P O 48 | * pushState 49 | * 50 | * Based on the above information, we only need to check for the case of a 51 | * redirect w/o pushState, in which case QWebFrame->url() will have the correct 52 | * current_url value. In all other cases QWebFrame->requestedUrl() is correct. 53 | */ 54 | void CurrentUrl::start() { 55 | QUrl humanUrl = wasRedirectedAndNotModifiedByJavascript() ? 56 | page()->currentFrame()->url() : page()->currentFrame()->requestedUrl(); 57 | QByteArray encodedBytes = humanUrl.toEncoded(); 58 | emit finished(new Response(true, encodedBytes)); 59 | } 60 | 61 | bool CurrentUrl::wasRegularLoad() { 62 | return page()->currentFrame()->url() == page()->currentFrame()->requestedUrl(); 63 | } 64 | 65 | bool CurrentUrl::wasRedirectedAndNotModifiedByJavascript() { 66 | return !wasRegularLoad() && page()->currentFrame()->url() == page()->history()->currentItem().url(); 67 | } 68 | 69 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/node.rb: -------------------------------------------------------------------------------- 1 | class Capybara::Driver::Webkit 2 | class Node < Capybara::Driver::Node 3 | NBSP = "\xC2\xA0" 4 | NBSP.force_encoding("UTF-8") if NBSP.respond_to?(:force_encoding) 5 | 6 | def text 7 | invoke("text").gsub(NBSP, ' ').gsub(/\s+/u, ' ').strip 8 | end 9 | 10 | def [](name) 11 | value = invoke("attribute", name) 12 | if name == 'checked' || name == 'disabled' 13 | value == 'true' 14 | else 15 | value 16 | end 17 | end 18 | 19 | def value 20 | if multiple_select? 21 | self.find(".//option").select(&:selected?).map(&:value) 22 | else 23 | invoke "value" 24 | end 25 | end 26 | 27 | def set(value) 28 | invoke "set", value 29 | end 30 | 31 | def select_option 32 | invoke "selectOption" 33 | end 34 | 35 | def unselect_option 36 | select = find("ancestor::select").first 37 | if select.multiple_select? 38 | invoke "unselectOption" 39 | else 40 | raise Capybara::UnselectNotAllowed 41 | end 42 | end 43 | 44 | def click 45 | invoke "click" 46 | end 47 | 48 | def drag_to(element) 49 | invoke 'dragTo', element.native 50 | end 51 | 52 | def tag_name 53 | invoke "tagName" 54 | end 55 | 56 | def visible? 57 | invoke("visible") == "true" 58 | end 59 | 60 | def selected? 61 | invoke("selected") == "true" 62 | end 63 | 64 | def checked? 65 | self['checked'] 66 | end 67 | 68 | def disabled? 69 | self['disabled'] 70 | end 71 | 72 | def path 73 | invoke "path" 74 | end 75 | 76 | def submit(opts = {}) 77 | invoke "submit" 78 | end 79 | 80 | def trigger(event) 81 | invoke "trigger", event 82 | end 83 | 84 | def find(xpath) 85 | invoke("findWithin", xpath).split(',').map do |native| 86 | self.class.new(driver, native) 87 | end 88 | end 89 | 90 | def invoke(name, *args) 91 | if allow_unattached_nodes? || attached? 92 | browser.command "Node", name, native, *args 93 | else 94 | raise Capybara::Driver::Webkit::NodeNotAttachedError 95 | end 96 | end 97 | 98 | def allow_unattached_nodes? 99 | !automatic_reload? 100 | end 101 | 102 | def automatic_reload? 103 | Capybara.respond_to?(:automatic_reload) && Capybara.automatic_reload 104 | end 105 | 106 | def attached? 107 | browser.command("Node", "isAttached", native) == "true" 108 | end 109 | 110 | def browser 111 | driver.browser 112 | end 113 | 114 | def multiple_select? 115 | self.tag_name == "select" && self["multiple"] == "multiple" 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2012-03-09 Joe Ferris 2 | * node.rb, driver_spec.rb: Allow interaction with invisible elements 3 | 4 | 2012-03-02 Joe Ferris 5 | * browser.rb: Use Timeout from stdlib since Capybara.timeout is being removed 6 | 7 | 2012-03-02 Matthew Mongeau 8 | * capybara_webkit_builder.rb: 9 | set LANG to en_US.UTF-8 to prevent string encoding issues during install 10 | 11 | 2012-03-02 Marc Schwieterman 12 | * Rakefile: pro, find_command, and CommandFactory are more structured 13 | 14 | 2012-02-27 Joe Ferris 15 | * README.md: Fixed broken wiki link 16 | 17 | 2012-02-24 Joe Ferris 18 | * README.md: Update instructions for platform specific installation issues 19 | 20 | 2012-02-17 Matthew Mongeau 21 | * driver_spec.rb, capybara.js: Send proper keycode during keypress event 22 | 23 | 2012-02-17 Matthew Mongeau 24 | * ChangeLog, version.rb: 25 | Bump to 0.10.0 26 | 27 | 2012-02-17 Marc Schwieterman 28 | * driver_spec.rb, Reset.cpp, Reset.h: Reset history when resetting session. 29 | * driver_spec.rb, webkit.rb, browser.rb, CommandFactory.cpp, CurrentUrl.cpp, 30 | CurrentUrl.h, find_command.h, webkit_server.pro: 31 | current_url now conforms more closely to the behavior of Selenium. 32 | 33 | 2012-02-17 Igor Zubkov 34 | * spec_helper.rb: Fix typo. 35 | 36 | 2012-02-17 Matthew Mongeau 37 | * capybara_webkit_builder.rb, capybara_webkit_builder_spec.rb, 38 | spec_helper.rb: 39 | Allow for overriding MAKE, QMAKE, and SPEC options via the environment. 40 | 41 | 2012-02-12 Jason Petersen 42 | * driver_spec.rb, node.rb, capybara.js: 43 | Selected attribute is no longer removed when selecting/deselecting. Only the 44 | property is changed. 45 | 46 | 2012-02-09 Gabe Berke-Williams 47 | * capybara-webkit.gemspec: Note capybara-webkit's usage of Sinatra. 48 | 49 | 2012-02-03 Matthew Mongeau 50 | * version.rb: 51 | Bump to 0.9.0 52 | 53 | 2012-02-01 Joe Ferris 54 | * driver_spec.rb, Connection.cpp, Connection.h: 55 | Try to detect when a command starts a page load and wait for it to finish. 56 | 57 | 2012-01-27 Matthew Mongeau 58 | * driver_spec.rb, capybara.js: Trigger mousedown and mouseup events. 59 | * driver_spec.rb, capybara.js: Simulate browser events more closely. 60 | 61 | 2012-01-19 Marco Antonio 62 | * node.rb, driver_spec.rb: 63 | Raise ElementNotDisplayedError also for #drag_to and #select_option when they are invisible. 64 | 65 | 2012-01-18 Marco Antonio 66 | * node.rb, driver_spec.rb: 67 | Raise error when an invisible element receives #click so it resembles a browser more closely. 68 | 69 | 2012-01-15 Marc Schwieterman 70 | * CONTRIBUTING.md: add imagemagick dependency to contributing guide. 71 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/driver/webkit/version" 3 | require "capybara/driver/webkit/node" 4 | require "capybara/driver/webkit/connection" 5 | require "capybara/driver/webkit/browser" 6 | require "capybara/driver/webkit/socket_debugger" 7 | require "capybara/driver/webkit/cookie_jar" 8 | 9 | class Capybara::Driver::Webkit 10 | class WebkitInvalidResponseError < StandardError 11 | end 12 | 13 | class WebkitNoResponseError < StandardError 14 | end 15 | 16 | class NodeNotAttachedError < Capybara::ElementNotFound 17 | end 18 | 19 | attr_reader :browser 20 | 21 | def initialize(app, options={}) 22 | @app = app 23 | @options = options 24 | @rack_server = Capybara::Server.new(@app) 25 | @rack_server.boot if Capybara.run_server 26 | @browser = options[:browser] || Browser.new(Connection.new(options)) 27 | @browser.ignore_ssl_errors if options[:ignore_ssl_errors] 28 | end 29 | 30 | def current_url 31 | browser.current_url 32 | end 33 | 34 | def requested_url 35 | browser.requested_url 36 | end 37 | 38 | def visit(path) 39 | browser.visit(url(path)) 40 | end 41 | 42 | def find(query) 43 | browser.find(query).map { |native| Node.new(self, native) } 44 | end 45 | 46 | def source 47 | browser.source 48 | end 49 | 50 | def body 51 | browser.body 52 | end 53 | 54 | def header(key, value) 55 | browser.header(key, value) 56 | end 57 | 58 | def execute_script(script) 59 | value = browser.execute_script script 60 | value.empty? ? nil : value 61 | end 62 | 63 | def evaluate_script(script) 64 | browser.evaluate_script script 65 | end 66 | 67 | def console_messages 68 | browser.console_messages 69 | end 70 | 71 | def error_messages 72 | browser.error_messages 73 | end 74 | 75 | def response_headers 76 | browser.response_headers 77 | end 78 | 79 | def status_code 80 | browser.status_code 81 | end 82 | 83 | def resize_window(width, height) 84 | browser.resize_window(width, height) 85 | end 86 | 87 | def within_frame(frame_id_or_index) 88 | browser.frame_focus(frame_id_or_index) 89 | begin 90 | yield 91 | ensure 92 | browser.frame_focus 93 | end 94 | end 95 | 96 | def within_window(handle) 97 | raise Capybara::NotSupportedByDriverError 98 | end 99 | 100 | def wait? 101 | true 102 | end 103 | 104 | def wait_until(*args) 105 | end 106 | 107 | def reset! 108 | browser.reset! 109 | end 110 | 111 | def has_shortcircuit_timeout? 112 | false 113 | end 114 | 115 | def render(path, options={}) 116 | options[:width] ||= 1000 117 | options[:height] ||= 10 118 | 119 | browser.render path, options[:width], options[:height] 120 | end 121 | 122 | def server_port 123 | @rack_server.port 124 | end 125 | 126 | def cookies 127 | @cookie_jar ||= CookieJar.new(browser) 128 | end 129 | 130 | private 131 | 132 | def url(path) 133 | @rack_server.url(path) 134 | end 135 | end 136 | 137 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/connection.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'timeout' 3 | require 'thread' 4 | 5 | class Capybara::Driver::Webkit 6 | class Connection 7 | WEBKIT_SERVER_START_TIMEOUT = 15 8 | 9 | attr_reader :port 10 | 11 | def initialize(options = {}) 12 | @socket_class = options[:socket_class] || TCPSocket 13 | @stdout = options.has_key?(:stdout) ? 14 | options[:stdout] : 15 | $stdout 16 | start_server 17 | connect 18 | end 19 | 20 | def puts(string) 21 | @socket.puts string 22 | end 23 | 24 | def print(string) 25 | @socket.print string 26 | end 27 | 28 | def gets 29 | @socket.gets 30 | end 31 | 32 | def read(length) 33 | @socket.read(length) 34 | end 35 | 36 | private 37 | 38 | def start_server 39 | pipe = fork_server 40 | @port = discover_port(pipe) 41 | @stdout_thread = Thread.new do 42 | Thread.current.abort_on_exception = true 43 | forward_stdout(pipe) 44 | end 45 | end 46 | 47 | def fork_server 48 | server_path = File.expand_path("../../../../../bin/webkit_server", __FILE__) 49 | pipe, @pid = server_pipe_and_pid(server_path) 50 | register_shutdown_hook 51 | pipe 52 | end 53 | 54 | def kill_process(pid) 55 | if RUBY_PLATFORM =~ /mingw32/ 56 | Process.kill(9, pid) 57 | else 58 | Process.kill("INT", pid) 59 | end 60 | end 61 | 62 | def register_shutdown_hook 63 | @owner_pid = Process.pid 64 | at_exit do 65 | if Process.pid == @owner_pid 66 | kill_process(@pid) 67 | end 68 | end 69 | end 70 | 71 | def server_pipe_and_pid(server_path) 72 | cmdline = [server_path] 73 | pipe = IO.popen(cmdline.join(" ")) 74 | [pipe, pipe.pid] 75 | end 76 | 77 | def discover_port(read_pipe) 78 | return unless IO.select([read_pipe], nil, nil, WEBKIT_SERVER_START_TIMEOUT) 79 | ((read_pipe.first || '').match(/listening on port: (\d+)/) || [])[1].to_i 80 | end 81 | 82 | def forward_stdout(pipe) 83 | while pipe_readable?(pipe) 84 | line = pipe.readline 85 | if @stdout 86 | @stdout.write(line) 87 | @stdout.flush 88 | end 89 | end 90 | rescue EOFError 91 | end 92 | 93 | def connect 94 | Timeout.timeout(5) do 95 | while @socket.nil? 96 | attempt_connect 97 | end 98 | end 99 | end 100 | 101 | def attempt_connect 102 | @socket = @socket_class.open("127.0.0.1", @port) 103 | rescue Errno::ECONNREFUSED 104 | end 105 | 106 | if !defined?(RUBY_ENGINE) || (RUBY_ENGINE == "ruby" && RUBY_VERSION <= "1.8") 107 | # please note the use of IO::select() here, as it is used specifically to 108 | # preserve correct signal handling behavior in ruby 1.8. 109 | # https://github.com/thibaudgg/rb-fsevent/commit/d1a868bf8dc72dbca102bedbadff76c7e6c2dc21 110 | # https://github.com/thibaudgg/rb-fsevent/blob/1ca42b987596f350ee7b19d8f8210b7b6ae8766b/ext/fsevent/fsevent_watch.c#L171 111 | def pipe_readable?(pipe) 112 | IO.select([pipe]) 113 | end 114 | else 115 | def pipe_readable?(pipe) 116 | !pipe.eof? 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /src/NetworkCookieJar.cpp: -------------------------------------------------------------------------------- 1 | #include "NetworkCookieJar.h" 2 | #include "QtCore/qdatetime.h" 3 | 4 | NetworkCookieJar::NetworkCookieJar(QObject *parent) 5 | : QNetworkCookieJar(parent) 6 | { } 7 | 8 | QList NetworkCookieJar::getAllCookies() const 9 | { 10 | return allCookies(); 11 | } 12 | 13 | void NetworkCookieJar::clearCookies() 14 | { 15 | setAllCookies(QList()); 16 | } 17 | 18 | static inline bool isParentDomain(QString domain, QString reference) 19 | { 20 | if (!reference.startsWith(QLatin1Char('.'))) 21 | return domain == reference; 22 | 23 | return domain.endsWith(reference) || domain == reference.mid(1); 24 | } 25 | 26 | void NetworkCookieJar::overwriteCookies(const QList& cookieList) 27 | { 28 | /* this function is basically a copy-and-paste of the original 29 | QNetworkCookieJar::setCookiesFromUrl with the domain and 30 | path validations removed */ 31 | 32 | QString defaultPath(QLatin1Char('/')); 33 | QDateTime now = QDateTime::currentDateTime(); 34 | QList newCookies = allCookies(); 35 | 36 | foreach (QNetworkCookie cookie, cookieList) { 37 | bool isDeletion = (!cookie.isSessionCookie() && 38 | cookie.expirationDate() < now); 39 | 40 | // validate the cookie & set the defaults if unset 41 | if (cookie.path().isEmpty()) 42 | cookie.setPath(defaultPath); 43 | 44 | // don't do path checking. See http://bugreports.qt.nokia.com/browse/QTBUG-5815 45 | // else if (!isParentPath(pathAndFileName, cookie.path())) { 46 | // continue; // not accepted 47 | // } 48 | 49 | if (cookie.domain().isEmpty()) { 50 | continue; 51 | } else { 52 | // Ensure the domain starts with a dot if its field was not empty 53 | // in the HTTP header. There are some servers that forget the 54 | // leading dot and this is actually forbidden according to RFC 2109, 55 | // but all browsers accept it anyway so we do that as well. 56 | if (!cookie.domain().startsWith(QLatin1Char('.'))) 57 | cookie.setDomain(QLatin1Char('.') + cookie.domain()); 58 | 59 | QString domain = cookie.domain(); 60 | 61 | // the check for effective TLDs makes the "embedded dot" rule from RFC 2109 section 4.3.2 62 | // redundant; the "leading dot" rule has been relaxed anyway, see above 63 | // we remove the leading dot for this check 64 | /* 65 | if (QNetworkCookieJarPrivate::isEffectiveTLD(domain.remove(0, 1))) 66 | continue; // not accepted 67 | */ 68 | } 69 | 70 | for (int i = 0; i < newCookies.size(); ++i) { 71 | // does this cookie already exist? 72 | const QNetworkCookie ¤t = newCookies.at(i); 73 | if (cookie.name() == current.name() && 74 | cookie.domain() == current.domain() && 75 | cookie.path() == current.path()) { 76 | // found a match 77 | newCookies.removeAt(i); 78 | break; 79 | } 80 | } 81 | 82 | // did not find a match 83 | if (!isDeletion) { 84 | int countForDomain = 0; 85 | for (int i = newCookies.size() - 1; i >= 0; --i) { 86 | // Start from the end and delete the oldest cookies to keep a maximum count of 50. 87 | const QNetworkCookie ¤t = newCookies.at(i); 88 | if (isParentDomain(cookie.domain(), current.domain()) 89 | || isParentDomain(current.domain(), cookie.domain())) { 90 | if (countForDomain >= 49) 91 | newCookies.removeAt(i); 92 | else 93 | ++countForDomain; 94 | } 95 | } 96 | 97 | newCookies += cookie; 98 | } 99 | } 100 | setAllCookies(newCookies); 101 | } 102 | -------------------------------------------------------------------------------- /lib/capybara/driver/webkit/browser.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class Capybara::Driver::Webkit 4 | class Browser 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def visit(url) 10 | command "Visit", url 11 | end 12 | 13 | def header(key, value) 14 | command("Header", key, value) 15 | end 16 | 17 | def find(query) 18 | command("Find", query).split(",") 19 | end 20 | 21 | def reset! 22 | command("Reset") 23 | end 24 | 25 | def body 26 | command("Body") 27 | end 28 | 29 | def source 30 | command("Source") 31 | end 32 | 33 | def status_code 34 | command("Status").to_i 35 | end 36 | 37 | def console_messages 38 | command("ConsoleMessages").split("\n").map do |messages| 39 | parts = messages.split("|", 3) 40 | { :source => parts.first, :line_number => Integer(parts[1]), :message => parts.last } 41 | end 42 | end 43 | 44 | def error_messages 45 | console_messages.select do |message| 46 | message[:message] =~ /Error:/ 47 | end 48 | end 49 | 50 | def response_headers 51 | Hash[command("Headers").split("\n").map { |header| header.split(": ") }] 52 | end 53 | 54 | def url 55 | command("Url") 56 | end 57 | 58 | def requested_url 59 | command("RequestedUrl") 60 | end 61 | 62 | def current_url 63 | command("CurrentUrl") 64 | end 65 | 66 | def frame_focus(frame_id_or_index=nil) 67 | if frame_id_or_index.is_a? Fixnum 68 | command("FrameFocus", "", frame_id_or_index.to_s) 69 | elsif frame_id_or_index 70 | command("FrameFocus", frame_id_or_index) 71 | else 72 | command("FrameFocus") 73 | end 74 | end 75 | 76 | def ignore_ssl_errors 77 | command("IgnoreSslErrors") 78 | end 79 | 80 | def command(name, *args) 81 | @connection.puts name 82 | @connection.puts args.size 83 | args.each do |arg| 84 | @connection.puts arg.to_s.bytesize 85 | @connection.print arg.to_s 86 | end 87 | check 88 | read_response 89 | end 90 | 91 | def evaluate_script(script) 92 | json = command('Evaluate', script) 93 | JSON.parse("[#{json}]").first 94 | end 95 | 96 | def execute_script(script) 97 | command('Execute', script) 98 | end 99 | 100 | def render(path, width, height) 101 | command "Render", path, width, height 102 | end 103 | 104 | def set_cookie(cookie) 105 | command "SetCookie", cookie 106 | end 107 | 108 | def clear_cookies 109 | command "ClearCookies" 110 | end 111 | 112 | def get_cookies 113 | command("GetCookies").lines.map{ |line| line.strip }.select{ |line| !line.empty? } 114 | end 115 | 116 | def set_proxy(options = {}) 117 | options = default_proxy_options.merge(options) 118 | command("SetProxy", options[:host], options[:port], options[:user], options[:pass]) 119 | end 120 | 121 | def clear_proxy 122 | command("SetProxy") 123 | end 124 | 125 | def resize_window(width, height) 126 | command("ResizeWindow", width.to_i, height.to_i) 127 | end 128 | 129 | private 130 | 131 | def check 132 | result = @connection.gets 133 | result.strip! if result 134 | 135 | if result.nil? 136 | raise WebkitNoResponseError, "No response received from the server." 137 | elsif result != 'ok' 138 | raise WebkitInvalidResponseError, read_response 139 | end 140 | 141 | result 142 | end 143 | 144 | def read_response 145 | response_length = @connection.gets.to_i 146 | if response_length > 0 147 | response = @connection.read(response_length) 148 | response.force_encoding("UTF-8") if response.respond_to?(:force_encoding) 149 | response 150 | else 151 | "" 152 | end 153 | end 154 | 155 | def default_proxy_options 156 | { 157 | :host => "localhost", 158 | :port => "0", 159 | :user => "", 160 | :pass => "" 161 | } 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/integration/session_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: UTF-8 -*- 2 | 3 | require 'spec_helper' 4 | require 'capybara/webkit' 5 | 6 | describe Capybara::Session do 7 | subject { Capybara::Session.new(:reusable_webkit, @app) } 8 | after { subject.reset! } 9 | 10 | context "slow javascript app" do 11 | before(:all) do 12 | @app = lambda do |env| 13 | body = <<-HTML 14 | 15 |
16 |

Hello

17 | 18 | 27 | 28 | HTML 29 | [200, 30 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 31 | [body]] 32 | end 33 | end 34 | 35 | before do 36 | @default_wait_time = Capybara.default_wait_time 37 | Capybara.default_wait_time = 1 38 | end 39 | 40 | after { Capybara.default_wait_time = @default_wait_time } 41 | 42 | it "waits for a request to load" do 43 | subject.visit("/") 44 | subject.find_button("Submit").click 45 | subject.should have_content("Goodbye"); 46 | end 47 | end 48 | 49 | context "simple app" do 50 | before(:all) do 51 | @app = lambda do |env| 52 | body = <<-HTML 53 | 54 | Hello 55 | UTF8文字列 56 | 57 | 58 | HTML 59 | [200, 60 | { 'Content-Type' => 'text/html; charset=UTF-8', 'Content-Length' => body.length.to_s }, 61 | [body]] 62 | end 63 | end 64 | 65 | before do 66 | subject.visit("/") 67 | end 68 | 69 | it "inspects nodes" do 70 | subject.all(:xpath, "//strong").first.inspect.should include("strong") 71 | end 72 | 73 | it "can read utf8 string" do 74 | utf8str = subject.all(:xpath, "//span").first.text 75 | utf8str.should eq('UTF8文字列') 76 | end 77 | 78 | it "can click utf8 string" do 79 | subject.click_button('ボタン') 80 | end 81 | end 82 | 83 | context "response headers with status code" do 84 | before(:all) do 85 | @app = lambda do |env| 86 | params = ::Rack::Utils.parse_query(env['QUERY_STRING']) 87 | if params["img"] == "true" 88 | body = 'not found' 89 | return [404, { 'Content-Type' => 'image/gif', 'Content-Length' => body.length.to_s }, [body]] 90 | end 91 | body = <<-HTML 92 | 93 | 94 | 95 | 96 | 97 | HTML 98 | [200, 99 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s, 'X-Capybara' => 'WebKit'}, 100 | [body]] 101 | end 102 | end 103 | 104 | it "should get status code" do 105 | subject.visit '/' 106 | subject.status_code.should == 200 107 | end 108 | 109 | it "should reset status code" do 110 | subject.visit '/' 111 | subject.status_code.should == 200 112 | subject.reset! 113 | subject.status_code.should == 0 114 | end 115 | 116 | it "should get response headers" do 117 | subject.visit '/' 118 | subject.response_headers['X-Capybara'].should == 'WebKit' 119 | end 120 | 121 | it "should reset response headers" do 122 | subject.visit '/' 123 | subject.response_headers['X-Capybara'].should == 'WebKit' 124 | subject.reset! 125 | subject.response_headers['X-Capybara'].should == nil 126 | end 127 | end 128 | end 129 | 130 | describe Capybara::Session, "with TestApp" do 131 | before do 132 | @session = Capybara::Session.new(:reusable_webkit, TestApp) 133 | end 134 | 135 | it_should_behave_like "session" 136 | it_should_behave_like "session with javascript support" 137 | end 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | capybara-webkit 2 | =============== 3 | 4 | A [capybara](https://github.com/jnicklas/capybara) driver that uses [WebKit](http://webkit.org) via [QtWebKit](http://doc.qt.nokia.com/4.7/qtwebkit.html). 5 | 6 | Qt Dependency and Installation Issues 7 | ------------- 8 | 9 | capybara-webkit depends on a WebKit implementation from Qt, a cross-platform 10 | development toolkit. You'll need to download the Qt libraries to build and 11 | install the gem. You can find instructions for downloading and installing QT on 12 | the [capybara-webkit wiki](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit) 13 | 14 | Windows Support 15 | --------------- 16 | 17 | Currently 32bit Windows will compile Capybara-webkit. Support for Windows is provided by the open source community and Windows related issues should be posted to the [mailing list](http://groups.google.com/group/capybara-webkit) 18 | 19 | Reporting Issues 20 | ---------------- 21 | 22 | Without access to your application code we can't easily debug most crashes or 23 | generic failures, so we've included a debug version of the driver that prints a 24 | log of what happened during each test. Before filing a crash bug, please see 25 | [Reporting Crashes](https://github.com/thoughtbot/capybara-webkit/wiki/Reporting-Crashes). 26 | You're much more likely to get a fix if you follow those instructions. 27 | 28 | If you are having compiling issues please check out the 29 | [capybara-webkit wiki](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit). 30 | If you don't have any luck there, please post to the 31 | [mailing list](http://groups.google.com/group/capybara-webkit). Please don't 32 | open a Github issue for a system-specific compiler issue. 33 | 34 | CI 35 | -- 36 | 37 | If you're like us, you'll be using capybara-webkit on CI. 38 | 39 | On Linux platforms, capybara-webkit requires an X server to run, although it doesn't create any visible windows. Xvfb works fine for this. You can setup Xvfb yourself and set a DISPLAY variable, or try out the [headless gem](https://github.com/leonid-shevtsov/headless). 40 | 41 | Usage 42 | ----- 43 | 44 | Add the capybara-webkit gem to your Gemfile: 45 | 46 | gem "capybara-webkit" 47 | 48 | Set your Capybara Javascript driver to webkit: 49 | 50 | Capybara.javascript_driver = :webkit 51 | 52 | In cucumber, tag scenarios with @javascript to run them using a headless WebKit browser. 53 | 54 | In RSpec, use the :js => true flag. 55 | 56 | Take note of the transactional fixtures section of the [capybara README](https://github.com/jnicklas/capybara/blob/master/README.md). 57 | 58 | If you're using capybara-webkit with Sinatra, don't forget to set 59 | 60 | Capybara.app = MySinatraApp.new 61 | 62 | Non-Standard Driver Methods 63 | --------------------------- 64 | 65 | capybara-webkit supports a few methods that are not part of the standard capybara API. You can access these by calling `driver` on the capybara session. When using the DSL, that will look like `page.driver.method_name`. 66 | 67 | **console_messages**: returns an array of messages printed using console.log 68 | 69 | # In Javascript: 70 | console.log("hello") 71 | # In Ruby: 72 | page.driver.console_messages 73 | => {:source=>"http://example.com", :line_number=>1, :message=>"hello"} 74 | 75 | **error_messages**: returns an array of Javascript errors that occurred 76 | 77 | page.driver.error_messages 78 | => {:source=>"http://example.com", :line_number=>1, :message=>"SyntaxError: Parse error"} 79 | 80 | **resize_window**: change the viewport size to the given width and height 81 | 82 | page.driver.resize_window(500, 300) 83 | page.driver.evaluate_script("window.innerWidth") 84 | => 500 85 | 86 | **render**: render a screenshot of the current view (requires [mini_magick](https://github.com/probablycorey/mini_magick) and [ImageMagick](http://www.imagemagick.org)) 87 | 88 | page.driver.render "tmp/screenshot.png" 89 | 90 | **cookies**: allows read-only access of cookies for the current session 91 | 92 | page.driver.cookies["alpha"] 93 | => "abc" 94 | 95 | Contributing 96 | ------------ 97 | 98 | See the CONTRIBUTING document. 99 | 100 | About 101 | ----- 102 | 103 | The capybara WebKit driver is maintained by Joe Ferris and Matt Mongeau. It was written by [thoughtbot, inc](http://thoughtbot.com/community) with the help of numerous [contributions from the open source community](https://github.com/thoughtbot/capybara-webkit/contributors). 104 | 105 | Code for rendering the current webpage to a PNG is borrowed from Phantom.js' implementation. 106 | 107 | ![thoughtbot](http://thoughtbot.com/images/tm/logo.png) 108 | 109 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 110 | 111 | License 112 | ------- 113 | 114 | capybara-webkit is Copyright (c) 2011 thoughtbot, inc. It is free software, and may be redistributed under the terms specified in the LICENSE file. 115 | -------------------------------------------------------------------------------- /spec/browser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'self_signed_ssl_cert' 3 | require 'stringio' 4 | require 'capybara/driver/webkit/browser' 5 | require 'capybara/driver/webkit/connection' 6 | require 'socket' 7 | require 'base64' 8 | 9 | describe Capybara::Driver::Webkit::Browser do 10 | 11 | let(:browser) { Capybara::Driver::Webkit::Browser.new(Capybara::Driver::Webkit::Connection.new) } 12 | let(:browser_ignore_ssl_err) do 13 | Capybara::Driver::Webkit::Browser.new(Capybara::Driver::Webkit::Connection.new).tap do |browser| 14 | browser.ignore_ssl_errors 15 | end 16 | end 17 | 18 | context 'handling of SSL validation errors' do 19 | before do 20 | # set up minimal HTTPS server 21 | @host = "127.0.0.1" 22 | @server = TCPServer.new(@host, 0) 23 | @port = @server.addr[1] 24 | 25 | # set up SSL layer 26 | ssl_serv = OpenSSL::SSL::SSLServer.new(@server, $openssl_self_signed_ctx) 27 | 28 | @server_thread = Thread.new(ssl_serv) do |serv| 29 | while conn = serv.accept do 30 | # read request 31 | request = [] 32 | until (line = conn.readline.strip).empty? 33 | request << line 34 | end 35 | 36 | # write response 37 | html = "D'oh!" 38 | conn.write "HTTP/1.1 200 OK\r\n" 39 | conn.write "Content-Type:text/html\r\n" 40 | conn.write "Content-Length: %i\r\n" % html.size 41 | conn.write "\r\n" 42 | conn.write html 43 | conn.close 44 | end 45 | end 46 | end 47 | 48 | after do 49 | @server_thread.kill 50 | @server.close 51 | end 52 | 53 | it "doesn't accept a self-signed certificate by default" do 54 | lambda { browser.visit "https://#{@host}:#{@port}/" }.should raise_error 55 | end 56 | 57 | it 'accepts a self-signed certificate if configured to do so' do 58 | browser_ignore_ssl_err.visit "https://#{@host}:#{@port}/" 59 | end 60 | end 61 | 62 | describe "forking", :skip_on_windows => true do 63 | it "only shuts down the server from the main process" do 64 | browser.reset! 65 | pid = fork {} 66 | Process.wait(pid) 67 | expect { browser.reset! }.not_to raise_error 68 | end 69 | end 70 | 71 | describe '#set_proxy' do 72 | before do 73 | @host = '127.0.0.1' 74 | @user = 'user' 75 | @pass = 'secret' 76 | @url = "http://example.org/" 77 | 78 | @server = TCPServer.new(@host, 0) 79 | @port = @server.addr[1] 80 | 81 | @proxy_requests = [] 82 | @proxy = Thread.new(@server, @proxy_requests) do |serv, proxy_requests| 83 | while conn = serv.accept do 84 | # read request 85 | request = [] 86 | until (line = conn.readline.strip).empty? 87 | request << line 88 | end 89 | 90 | # send response 91 | auth_header = request.find { |h| h =~ /Authorization:/i } 92 | if auth_header || request[0].split(/\s+/)[1] =~ /^\// 93 | html = "D'oh!" 94 | conn.write "HTTP/1.1 200 OK\r\n" 95 | conn.write "Content-Type:text/html\r\n" 96 | conn.write "Content-Length: %i\r\n" % html.size 97 | conn.write "\r\n" 98 | conn.write html 99 | conn.close 100 | proxy_requests << request if auth_header 101 | else 102 | conn.write "HTTP/1.1 407 Proxy Auth Required\r\n" 103 | conn.write "Proxy-Authenticate: Basic realm=\"Proxy\"\r\n" 104 | conn.write "\r\n" 105 | conn.close 106 | proxy_requests << request 107 | end 108 | end 109 | end 110 | 111 | browser.set_proxy(:host => @host, 112 | :port => @port, 113 | :user => @user, 114 | :pass => @pass) 115 | browser.visit @url 116 | @proxy_requests.size.should == 2 117 | @request = @proxy_requests[-1] 118 | end 119 | 120 | after do 121 | @proxy.kill 122 | @server.close 123 | end 124 | 125 | it 'uses the HTTP proxy correctly' do 126 | @request[0].should match /^GET\s+http:\/\/example.org\/\s+HTTP/i 127 | @request.find { |header| 128 | header =~ /^Host:\s+example.org$/i }.should_not be nil 129 | end 130 | 131 | it 'sends correct proxy authentication' do 132 | auth_header = @request.find { |header| 133 | header =~ /^Proxy-Authorization:\s+/i } 134 | auth_header.should_not be nil 135 | 136 | user, pass = Base64.decode64(auth_header.split(/\s+/)[-1]).split(":") 137 | user.should == @user 138 | pass.should == @pass 139 | end 140 | 141 | it "uses the proxies' response" do 142 | browser.body.should include "D'oh!" 143 | end 144 | 145 | it 'uses original URL' do 146 | browser.url.should == @url 147 | end 148 | 149 | it 'uses URLs changed by javascript' do 150 | browser.execute_script "window.history.pushState('', '', '/blah')" 151 | browser.requested_url.should == 'http://example.org/blah' 152 | end 153 | 154 | it 'is possible to disable proxy again' do 155 | @proxy_requests.clear 156 | browser.clear_proxy 157 | browser.visit "http://#{@host}:#{@port}/" 158 | @proxy_requests.size.should == 0 159 | end 160 | end 161 | 162 | it "doesn't try to read an empty response" do 163 | connection = stub("connection") 164 | connection.stub(:puts) 165 | connection.stub(:print) 166 | connection.stub(:gets).and_return("ok\n", "0\n") 167 | connection.stub(:read).and_raise(StandardError.new("tried to read empty response")) 168 | 169 | browser = Capybara::Driver::Webkit::Browser.new(connection) 170 | 171 | expect { browser.visit("/") }.not_to raise_error(/empty response/) 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /src/WebPage.cpp: -------------------------------------------------------------------------------- 1 | #include "WebPage.h" 2 | #include "JavascriptInvocation.h" 3 | #include "NetworkAccessManager.h" 4 | #include "NetworkCookieJar.h" 5 | #include "UnsupportedContentHandler.h" 6 | #include 7 | #include 8 | 9 | WebPage::WebPage(QObject *parent) : QWebPage(parent) { 10 | setForwardUnsupportedContent(true); 11 | loadJavascript(); 12 | setUserStylesheet(); 13 | 14 | m_loading = false; 15 | m_ignoreSslErrors = false; 16 | this->setCustomNetworkAccessManager(); 17 | 18 | connect(this, SIGNAL(loadStarted()), this, SLOT(loadStarted())); 19 | connect(this, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool))); 20 | connect(this, SIGNAL(frameCreated(QWebFrame *)), 21 | this, SLOT(frameCreated(QWebFrame *))); 22 | connect(this, SIGNAL(unsupportedContent(QNetworkReply*)), 23 | this, SLOT(handleUnsupportedContent(QNetworkReply*))); 24 | resetWindowSize(); 25 | } 26 | 27 | void WebPage::resetWindowSize() { 28 | this->setViewportSize(QSize(1680, 1050)); 29 | this->settings()->setAttribute(QWebSettings::LocalStorageDatabaseEnabled, true); 30 | } 31 | 32 | void WebPage::setCustomNetworkAccessManager() { 33 | NetworkAccessManager *manager = new NetworkAccessManager(); 34 | manager->setCookieJar(new NetworkCookieJar()); 35 | this->setNetworkAccessManager(manager); 36 | connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(replyFinished(QNetworkReply *))); 37 | connect(manager, SIGNAL(sslErrors(QNetworkReply *, QList)), 38 | this, SLOT(handleSslErrorsForReply(QNetworkReply *, QList))); 39 | } 40 | 41 | void WebPage::loadJavascript() { 42 | QResource javascript(":/capybara.js"); 43 | if (javascript.isCompressed()) { 44 | QByteArray uncompressedBytes(qUncompress(javascript.data(), javascript.size())); 45 | m_capybaraJavascript = QString(uncompressedBytes); 46 | } else { 47 | char * javascriptString = new char[javascript.size() + 1]; 48 | strcpy(javascriptString, (const char *)javascript.data()); 49 | javascriptString[javascript.size()] = 0; 50 | m_capybaraJavascript = javascriptString; 51 | } 52 | } 53 | 54 | void WebPage::setUserStylesheet() { 55 | QString data = QString("* { font-family: 'Arial' ! important; }").toUtf8().toBase64(); 56 | QUrl url = QUrl(QString("data:text/css;charset=utf-8;base64,") + data); 57 | settings()->setUserStyleSheetUrl(url); 58 | } 59 | 60 | QString WebPage::userAgentForUrl(const QUrl &url ) const { 61 | if (!m_userAgent.isEmpty()) { 62 | return m_userAgent; 63 | } else { 64 | return QWebPage::userAgentForUrl(url); 65 | } 66 | } 67 | 68 | QString WebPage::consoleMessages() { 69 | return m_consoleMessages.join("\n"); 70 | } 71 | 72 | void WebPage::setUserAgent(QString userAgent) { 73 | m_userAgent = userAgent; 74 | } 75 | 76 | void WebPage::frameCreated(QWebFrame * frame) { 77 | connect(frame, SIGNAL(javaScriptWindowObjectCleared()), 78 | this, SLOT(injectJavascriptHelpers())); 79 | } 80 | 81 | void WebPage::injectJavascriptHelpers() { 82 | QWebFrame* frame = qobject_cast(QObject::sender()); 83 | frame->evaluateJavaScript(m_capybaraJavascript); 84 | } 85 | 86 | bool WebPage::shouldInterruptJavaScript() { 87 | return false; 88 | } 89 | 90 | QVariant WebPage::invokeCapybaraFunction(const char *name, QStringList &arguments) { 91 | QString qname(name); 92 | QString objectName("CapybaraInvocation"); 93 | JavascriptInvocation invocation(qname, arguments); 94 | currentFrame()->addToJavaScriptWindowObject(objectName, &invocation); 95 | QString javascript = QString("Capybara.invoke()"); 96 | return currentFrame()->evaluateJavaScript(javascript); 97 | } 98 | 99 | QVariant WebPage::invokeCapybaraFunction(QString &name, QStringList &arguments) { 100 | return invokeCapybaraFunction(name.toAscii().data(), arguments); 101 | } 102 | 103 | void WebPage::javaScriptConsoleMessage(const QString &message, int lineNumber, const QString &sourceID) { 104 | QString fullMessage = QString::number(lineNumber) + "|" + message; 105 | if (!sourceID.isEmpty()) 106 | fullMessage = sourceID + "|" + fullMessage; 107 | m_consoleMessages.append(fullMessage); 108 | std::cout << qPrintable(fullMessage) << std::endl; 109 | } 110 | 111 | void WebPage::javaScriptAlert(QWebFrame *frame, const QString &message) { 112 | Q_UNUSED(frame); 113 | std::cout << "ALERT: " << qPrintable(message) << std::endl; 114 | } 115 | 116 | bool WebPage::javaScriptConfirm(QWebFrame *frame, const QString &message) { 117 | Q_UNUSED(frame); 118 | Q_UNUSED(message); 119 | return true; 120 | } 121 | 122 | bool WebPage::javaScriptPrompt(QWebFrame *frame, const QString &message, const QString &defaultValue, QString *result) { 123 | Q_UNUSED(frame) 124 | Q_UNUSED(message) 125 | Q_UNUSED(defaultValue) 126 | Q_UNUSED(result) 127 | return false; 128 | } 129 | 130 | void WebPage::loadStarted() { 131 | m_loading = true; 132 | } 133 | 134 | void WebPage::loadFinished(bool success) { 135 | m_loading = false; 136 | emit pageFinished(success); 137 | } 138 | 139 | bool WebPage::isLoading() const { 140 | return m_loading; 141 | } 142 | 143 | QString WebPage::failureString() { 144 | return QString("Unable to load URL: ") + currentFrame()->requestedUrl().toString(); 145 | } 146 | 147 | bool WebPage::render(const QString &fileName) { 148 | QFileInfo fileInfo(fileName); 149 | QDir dir; 150 | dir.mkpath(fileInfo.absolutePath()); 151 | 152 | QSize viewportSize = this->viewportSize(); 153 | QSize pageSize = this->mainFrame()->contentsSize(); 154 | if (pageSize.isEmpty()) { 155 | return false; 156 | } 157 | 158 | QImage buffer(pageSize, QImage::Format_ARGB32); 159 | buffer.fill(qRgba(255, 255, 255, 0)); 160 | 161 | QPainter p(&buffer); 162 | p.setRenderHint( QPainter::Antialiasing, true); 163 | p.setRenderHint( QPainter::TextAntialiasing, true); 164 | p.setRenderHint( QPainter::SmoothPixmapTransform, true); 165 | 166 | this->setViewportSize(pageSize); 167 | this->mainFrame()->render(&p); 168 | p.end(); 169 | this->setViewportSize(viewportSize); 170 | 171 | return buffer.save(fileName); 172 | } 173 | 174 | QString WebPage::chooseFile(QWebFrame *parentFrame, const QString &suggestedFile) { 175 | Q_UNUSED(parentFrame); 176 | Q_UNUSED(suggestedFile); 177 | 178 | return getLastAttachedFileName(); 179 | } 180 | 181 | bool WebPage::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output) { 182 | Q_UNUSED(option); 183 | if (extension == ChooseMultipleFilesExtension) { 184 | QStringList names = QStringList() << getLastAttachedFileName(); 185 | static_cast(output)->fileNames = names; 186 | return true; 187 | } 188 | return false; 189 | } 190 | 191 | QString WebPage::getLastAttachedFileName() { 192 | return currentFrame()->evaluateJavaScript(QString("Capybara.lastAttachedFile")).toString(); 193 | } 194 | 195 | void WebPage::replyFinished(QNetworkReply *reply) { 196 | if (reply->url() == this->currentFrame()->url()) { 197 | QStringList headers; 198 | m_lastStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 199 | QList list = reply->rawHeaderList(); 200 | 201 | int length = list.size(); 202 | for(int i = 0; i < length; i++) { 203 | headers << list.at(i)+": "+reply->rawHeader(list.at(i)); 204 | } 205 | 206 | m_pageHeaders = headers.join("\n"); 207 | } 208 | } 209 | 210 | void WebPage::handleSslErrorsForReply(QNetworkReply *reply, const QList &errors) { 211 | if (m_ignoreSslErrors) 212 | reply->ignoreSslErrors(errors); 213 | } 214 | 215 | void WebPage::ignoreSslErrors() { 216 | m_ignoreSslErrors = true; 217 | } 218 | 219 | int WebPage::getLastStatus() { 220 | return m_lastStatus; 221 | } 222 | 223 | void WebPage::resetResponseHeaders() { 224 | m_lastStatus = 0; 225 | m_pageHeaders = QString(); 226 | } 227 | 228 | void WebPage::resetConsoleMessages() { 229 | m_consoleMessages.clear(); 230 | } 231 | 232 | QString WebPage::pageHeaders() { 233 | return m_pageHeaders; 234 | } 235 | 236 | void WebPage::handleUnsupportedContent(QNetworkReply *reply) { 237 | UnsupportedContentHandler *handler = new UnsupportedContentHandler(this, reply); 238 | Q_UNUSED(handler); 239 | } 240 | -------------------------------------------------------------------------------- /src/capybara.js: -------------------------------------------------------------------------------- 1 | Capybara = { 2 | nextIndex: 0, 3 | nodes: {}, 4 | lastAttachedFile: "", 5 | 6 | invoke: function () { 7 | return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments); 8 | }, 9 | 10 | find: function (xpath) { 11 | return this.findRelativeTo(document, xpath); 12 | }, 13 | 14 | findWithin: function (index, xpath) { 15 | return this.findRelativeTo(this.nodes[index], xpath); 16 | }, 17 | 18 | findRelativeTo: function (reference, xpath) { 19 | var iterator = document.evaluate(xpath, reference, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 20 | var node; 21 | var results = []; 22 | while (node = iterator.iterateNext()) { 23 | this.nextIndex++; 24 | this.nodes[this.nextIndex] = node; 25 | results.push(this.nextIndex); 26 | } 27 | return results.join(","); 28 | }, 29 | 30 | isAttached: function(index) { 31 | return document.evaluate("ancestor-or-self::html", this.nodes[index], null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue != null; 32 | }, 33 | 34 | text: function (index) { 35 | var node = this.nodes[index]; 36 | var type = (node.type || node.tagName).toLowerCase(); 37 | if (type == "textarea") { 38 | return node.innerHTML; 39 | } else { 40 | return node.innerText; 41 | } 42 | }, 43 | 44 | attribute: function (index, name) { 45 | switch(name) { 46 | case 'checked': 47 | return this.nodes[index].checked; 48 | break; 49 | 50 | case 'disabled': 51 | return this.nodes[index].disabled; 52 | break; 53 | 54 | default: 55 | return this.nodes[index].getAttribute(name); 56 | } 57 | }, 58 | 59 | path: function(index) { 60 | return "/" + this.getXPathNode(this.nodes[index]).join("/"); 61 | }, 62 | 63 | getXPathNode: function(node, path) { 64 | path = path || []; 65 | if (node.parentNode) { 66 | path = this.getXPathNode(node.parentNode, path); 67 | } 68 | 69 | var first = node; 70 | while (first.previousSibling) 71 | first = first.previousSibling; 72 | 73 | var count = 0; 74 | var index = 0; 75 | var iter = first; 76 | while (iter) { 77 | if (iter.nodeType == 1 && iter.nodeName == node.nodeName) 78 | count++; 79 | if (iter.isSameNode(node)) 80 | index = count; 81 | iter = iter.nextSibling; 82 | continue; 83 | } 84 | 85 | if (node.nodeType == 1) 86 | path.push(node.nodeName.toLowerCase() + (node.id ? "[@id='"+node.id+"']" : count > 1 ? "["+index+"]" : '')); 87 | 88 | return path; 89 | }, 90 | 91 | tagName: function(index) { 92 | return this.nodes[index].tagName.toLowerCase(); 93 | }, 94 | 95 | submit: function(index) { 96 | return this.nodes[index].submit(); 97 | }, 98 | 99 | mousedown: function(index) { 100 | var mousedownEvent = document.createEvent('MouseEvents'); 101 | mousedownEvent.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 102 | this.nodes[index].dispatchEvent(mousedownEvent); 103 | }, 104 | 105 | mouseup: function(index) { 106 | var mouseupEvent = document.createEvent('MouseEvents'); 107 | mouseupEvent.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 108 | this.nodes[index].dispatchEvent(mouseupEvent); 109 | }, 110 | 111 | click: function (index) { 112 | this.mousedown(index); 113 | this.mouseup(index); 114 | var clickEvent = document.createEvent('MouseEvents'); 115 | clickEvent.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 116 | this.nodes[index].dispatchEvent(clickEvent); 117 | }, 118 | 119 | trigger: function (index, eventName) { 120 | var eventObject = document.createEvent("HTMLEvents"); 121 | eventObject.initEvent(eventName, true, true); 122 | this.nodes[index].dispatchEvent(eventObject); 123 | }, 124 | 125 | keypress: function(index, altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) { 126 | var eventObject = document.createEvent("Events"); 127 | eventObject.initEvent('keypress', true, true); 128 | eventObject.window = window; 129 | eventObject.altKey = altKey; 130 | eventObject.ctrlKey = ctrlKey; 131 | eventObject.shiftKey = shiftKey; 132 | eventObject.metaKey = metaKey; 133 | eventObject.keyCode = keyCode; 134 | eventObject.charCode = charCode; 135 | eventObject.which = keyCode; 136 | this.nodes[index].dispatchEvent(eventObject); 137 | }, 138 | 139 | keyupdown: function(index, eventName, keyCode) { 140 | var eventObject = document.createEvent("HTMLEvents"); 141 | eventObject.initEvent(eventName, true, true); 142 | eventObject.keyCode = keyCode; 143 | eventObject.which = keyCode; 144 | eventObject.charCode = 0; 145 | this.nodes[index].dispatchEvent(eventObject); 146 | }, 147 | 148 | visible: function (index) { 149 | var element = this.nodes[index]; 150 | while (element) { 151 | if (element.ownerDocument.defaultView.getComputedStyle(element, null).getPropertyValue("display") == 'none') 152 | return false; 153 | element = element.parentElement; 154 | } 155 | return true; 156 | }, 157 | 158 | selected: function (index) { 159 | return this.nodes[index].selected; 160 | }, 161 | 162 | value: function(index) { 163 | return this.nodes[index].value; 164 | }, 165 | 166 | characterToKeyCode: function(character) { 167 | var code = character.toUpperCase().charCodeAt(0); 168 | var specialKeys = { 169 | 96: 192, //` 170 | 45: 189, //- 171 | 61: 187, //= 172 | 91: 219, //[ 173 | 93: 221, //] 174 | 92: 220, //\ 175 | 59: 186, //; 176 | 39: 222, //' 177 | 44: 188, //, 178 | 46: 190, //. 179 | 47: 191, /// 180 | 127: 46, //delete 181 | 126: 192, //~ 182 | 33: 49, //! 183 | 64: 50, //@ 184 | 35: 51, //# 185 | 36: 52, //$ 186 | 37: 53, //% 187 | 94: 54, //^ 188 | 38: 55, //& 189 | 42: 56, //* 190 | 40: 57, //( 191 | 41: 48, //) 192 | 95: 189, //_ 193 | 43: 187, //+ 194 | 123: 219, //{ 195 | 125: 221, //} 196 | 124: 220, //| 197 | 58: 186, //: 198 | 34: 222, //" 199 | 60: 188, //< 200 | 62: 190, //> 201 | 63: 191 //? 202 | }; 203 | if (specialKeys[code]) { 204 | code = specialKeys[code]; 205 | } 206 | return code; 207 | }, 208 | 209 | set: function (index, value) { 210 | var length, maxLength, node, strindex, textTypes, type; 211 | 212 | node = this.nodes[index]; 213 | type = (node.type || node.tagName).toLowerCase(); 214 | textTypes = ["email", "number", "password", "search", "tel", "text", "textarea", "url"]; 215 | 216 | if (textTypes.indexOf(type) != -1) { 217 | this.trigger(index, "focus"); 218 | 219 | maxLength = this.attribute(index, "maxlength"); 220 | if (maxLength && value.length > maxLength) { 221 | length = maxLength; 222 | } else { 223 | length = value.length; 224 | } 225 | 226 | node.value = ""; 227 | for (strindex = 0; strindex < length; strindex++) { 228 | node.value += value[strindex]; 229 | var keyCode = this.characterToKeyCode(value[strindex]); 230 | this.keyupdown(index, "keydown", keyCode); 231 | this.keypress(index, false, false, false, false, value.charCodeAt(strindex), value.charCodeAt(strindex)); 232 | this.keyupdown(index, "keyup", keyCode); 233 | this.trigger(index, "input"); 234 | } 235 | this.trigger(index, "change"); 236 | this.trigger(index, "blur"); 237 | 238 | } else if (type === "checkbox" || type === "radio") { 239 | if (node.checked != (value === "true")) { 240 | this.click(index) 241 | } 242 | 243 | } else if (type === "file") { 244 | this.lastAttachedFile = value; 245 | this.click(index) 246 | 247 | } else { 248 | node.value = value; 249 | } 250 | }, 251 | 252 | selectOption: function(index) { 253 | this.nodes[index].selected = true; 254 | this.trigger(index, "change"); 255 | }, 256 | 257 | unselectOption: function(index) { 258 | this.nodes[index].selected = false; 259 | this.trigger(index, "change"); 260 | }, 261 | 262 | centerPostion: function(element) { 263 | this.reflow(element); 264 | var rect = element.getBoundingClientRect(); 265 | var position = { 266 | x: rect.width / 2, 267 | y: rect.height / 2 268 | }; 269 | do { 270 | position.x += element.offsetLeft; 271 | position.y += element.offsetTop; 272 | } while ((element = element.offsetParent)); 273 | position.x = Math.floor(position.x), position.y = Math.floor(position.y); 274 | 275 | return position; 276 | }, 277 | 278 | reflow: function(element, force) { 279 | if (force || element.offsetWidth === 0) { 280 | var prop, oldStyle = {}, newStyle = {position: "absolute", visibility : "hidden", display: "block" }; 281 | for (prop in newStyle) { 282 | oldStyle[prop] = element.style[prop]; 283 | element.style[prop] = newStyle[prop]; 284 | } 285 | element.offsetWidth, element.offsetHeight; // force reflow 286 | for (prop in oldStyle) 287 | element.style[prop] = oldStyle[prop]; 288 | } 289 | }, 290 | 291 | dragTo: function (index, targetIndex) { 292 | var element = this.nodes[index], target = this.nodes[targetIndex]; 293 | var position = this.centerPostion(element); 294 | var options = { 295 | clientX: position.x, 296 | clientY: position.y 297 | }; 298 | var mouseTrigger = function(eventName, options) { 299 | var eventObject = document.createEvent("MouseEvents"); 300 | eventObject.initMouseEvent(eventName, true, true, window, 0, 0, 0, options.clientX || 0, options.clientY || 0, false, false, false, false, 0, null); 301 | element.dispatchEvent(eventObject); 302 | } 303 | mouseTrigger('mousedown', options); 304 | options.clientX += 1, options.clientY += 1; 305 | mouseTrigger('mousemove', options); 306 | 307 | position = this.centerPostion(target), options = { 308 | clientX: position.x, 309 | clientY: position.y 310 | }; 311 | mouseTrigger('mousemove', options); 312 | mouseTrigger('mouseup', options); 313 | } 314 | }; 315 | 316 | -------------------------------------------------------------------------------- /spec/driver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara/driver/webkit' 3 | 4 | describe Capybara::Driver::Webkit do 5 | subject { Capybara::Driver::Webkit.new(@app, :browser => $webkit_browser) } 6 | before { subject.visit("/hello/world?success=true") } 7 | after { subject.reset! } 8 | 9 | context "iframe app" do 10 | before(:all) do 11 | @app = lambda do |env| 12 | params = ::Rack::Utils.parse_query(env['QUERY_STRING']) 13 | if params["iframe"] == "true" 14 | # We are in an iframe request. 15 | p_id = "farewell" 16 | msg = "goodbye" 17 | iframe = nil 18 | else 19 | # We are not in an iframe request and need to make an iframe! 20 | p_id = "greeting" 21 | msg = "hello" 22 | iframe = "" 23 | end 24 | body = <<-HTML 25 | 26 | 27 | 30 | 31 | 32 | #{iframe} 33 | 36 | 37 | 38 | HTML 39 | [200, 40 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 41 | [body]] 42 | end 43 | end 44 | 45 | it "finds frames by index" do 46 | subject.within_frame(0) do 47 | subject.find("//*[contains(., 'goodbye')]").should_not be_empty 48 | end 49 | end 50 | 51 | it "finds frames by id" do 52 | subject.within_frame("f") do 53 | subject.find("//*[contains(., 'goodbye')]").should_not be_empty 54 | end 55 | end 56 | 57 | it "raises error for missing frame by index" do 58 | expect { subject.within_frame(1) { } }. 59 | to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError) 60 | end 61 | 62 | it "raise_error for missing frame by id" do 63 | expect { subject.within_frame("foo") { } }. 64 | to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError) 65 | end 66 | 67 | it "returns an attribute's value" do 68 | subject.within_frame("f") do 69 | subject.find("//p").first["id"].should == "farewell" 70 | end 71 | end 72 | 73 | it "returns a node's text" do 74 | subject.within_frame("f") do 75 | subject.find("//p").first.text.should == "goodbye" 76 | end 77 | end 78 | 79 | it "returns the current URL" do 80 | subject.within_frame("f") do 81 | port = subject.instance_variable_get("@rack_server").port 82 | subject.current_url.should == "http://127.0.0.1:#{port}/?iframe=true" 83 | end 84 | end 85 | 86 | it "returns the source code for the page" do 87 | subject.within_frame("f") do 88 | subject.source.should =~ %r{.*farewell.*}m 89 | end 90 | end 91 | 92 | it "evaluates Javascript" do 93 | subject.within_frame("f") do 94 | result = subject.evaluate_script(%) 95 | result.should == "goodbye" 96 | end 97 | end 98 | 99 | it "executes Javascript" do 100 | subject.within_frame("f") do 101 | subject.execute_script(%) 102 | subject.find("//p[contains(., 'yo')]").should_not be_empty 103 | end 104 | end 105 | end 106 | 107 | context "redirect app" do 108 | before(:all) do 109 | @app = lambda do |env| 110 | if env['PATH_INFO'] == '/target' 111 | content_type = "

#{env['CONTENT_TYPE']}

" 112 | [200, {"Content-Type" => "text/html", "Content-Length" => content_type.length.to_s}, [content_type]] 113 | elsif env['PATH_INFO'] == '/form' 114 | body = <<-HTML 115 | 116 | 117 |
118 | 119 |
120 | 121 | 122 | HTML 123 | [200, {"Content-Type" => "text/html", "Content-Length" => body.length.to_s}, [body]] 124 | else 125 | [301, {"Location" => "/target"}, [""]] 126 | end 127 | end 128 | end 129 | 130 | it "should redirect without content type" do 131 | subject.visit("/form") 132 | subject.find("//input").first.click 133 | subject.find("//p").first.text.should == "" 134 | end 135 | 136 | it "returns the current URL when changed by pushState after a redirect" do 137 | subject.visit("/redirect-me") 138 | port = subject.instance_variable_get("@rack_server").port 139 | subject.execute_script("window.history.pushState({}, '', '/pushed-after-redirect')") 140 | subject.current_url.should == "http://127.0.0.1:#{port}/pushed-after-redirect" 141 | end 142 | 143 | it "returns the current URL when changed by replaceState after a redirect" do 144 | subject.visit("/redirect-me") 145 | port = subject.instance_variable_get("@rack_server").port 146 | subject.execute_script("window.history.replaceState({}, '', '/replaced-after-redirect')") 147 | subject.current_url.should == "http://127.0.0.1:#{port}/replaced-after-redirect" 148 | end 149 | end 150 | 151 | context "css app" do 152 | before(:all) do 153 | body = "css" 154 | @app = lambda do |env| 155 | [200, {"Content-Type" => "text/css", "Content-Length" => body.length.to_s}, [body]] 156 | end 157 | subject.visit("/") 158 | end 159 | 160 | it "renders unsupported content types gracefully" do 161 | subject.body.should =~ /css/ 162 | end 163 | 164 | it "sets the response headers with respect to the unsupported request" do 165 | subject.response_headers["Content-Type"].should == "text/css" 166 | end 167 | end 168 | 169 | context "hello app" do 170 | before(:all) do 171 | @app = lambda do |env| 172 | body = <<-HTML 173 | 174 | 175 | 178 | 179 | 180 |
Spaces not normalized 
181 |
182 |
Can't see me
183 |
184 | 185 | 186 | 189 | 190 | 191 | HTML 192 | [200, 193 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 194 | [body]] 195 | end 196 | end 197 | 198 | it "handles anchor tags" do 199 | subject.visit("#test") 200 | subject.find("//*[contains(., 'hello')]").should_not be_empty 201 | subject.visit("#test") 202 | subject.find("//*[contains(., 'hello')]").should_not be_empty 203 | end 204 | 205 | it "finds content after loading a URL" do 206 | subject.find("//*[contains(., 'hello')]").should_not be_empty 207 | end 208 | 209 | it "has an empty page after reseting" do 210 | subject.reset! 211 | subject.find("//*[contains(., 'hello')]").should be_empty 212 | end 213 | 214 | it "has a location of 'about:blank' after reseting" do 215 | subject.reset! 216 | subject.current_url.should == "about:blank" 217 | end 218 | 219 | it "raises an error for an invalid xpath query" do 220 | expect { subject.find("totally invalid salad") }. 221 | to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, /xpath/i) 222 | end 223 | 224 | it "returns an attribute's value" do 225 | subject.find("//p").first["id"].should == "greeting" 226 | end 227 | 228 | it "parses xpath with quotes" do 229 | subject.find('//*[contains(., "hello")]').should_not be_empty 230 | end 231 | 232 | it "returns a node's text" do 233 | subject.find("//p").first.text.should == "hello" 234 | end 235 | 236 | it "normalizes a node's text" do 237 | subject.find("//div[contains(@class, 'normalize')]").first.text.should == "Spaces not normalized" 238 | end 239 | 240 | it "returns the current URL" do 241 | port = subject.instance_variable_get("@rack_server").port 242 | subject.current_url.should == "http://127.0.0.1:#{port}/hello/world?success=true" 243 | end 244 | 245 | it "returns the current URL when changed by pushState" do 246 | port = subject.instance_variable_get("@rack_server").port 247 | subject.execute_script("window.history.pushState({}, '', '/pushed')") 248 | subject.current_url.should == "http://127.0.0.1:#{port}/pushed" 249 | end 250 | 251 | it "returns the current URL when changed by replaceState" do 252 | port = subject.instance_variable_get("@rack_server").port 253 | subject.execute_script("window.history.replaceState({}, '', '/replaced')") 254 | subject.current_url.should == "http://127.0.0.1:#{port}/replaced" 255 | end 256 | 257 | it "does not double-encode URLs" do 258 | subject.visit("/hello/world?success=%25true") 259 | subject.current_url.should =~ /success=\%25true/ 260 | end 261 | 262 | it "visits a page with an anchor" do 263 | subject.visit("/hello#display_none") 264 | subject.current_url.should =~ /hello#display_none/ 265 | end 266 | 267 | it "returns the source code for the page" do 268 | subject.source.should =~ %r{.*greeting.*}m 269 | end 270 | 271 | it "evaluates Javascript and returns a string" do 272 | result = subject.evaluate_script(%) 273 | result.should == "hello" 274 | end 275 | 276 | it "evaluates Javascript and returns an array" do 277 | result = subject.evaluate_script(%<["hello", "world"]>) 278 | result.should == %w(hello world) 279 | end 280 | 281 | it "evaluates Javascript and returns an int" do 282 | result = subject.evaluate_script(%<123>) 283 | result.should == 123 284 | end 285 | 286 | it "evaluates Javascript and returns a float" do 287 | result = subject.evaluate_script(%<1.5>) 288 | result.should == 1.5 289 | end 290 | 291 | it "evaluates Javascript and returns null" do 292 | result = subject.evaluate_script(%<(function () {})()>) 293 | result.should == nil 294 | end 295 | 296 | it "evaluates Javascript and returns an object" do 297 | result = subject.evaluate_script(%<({ 'one' : 1 })>) 298 | result.should == { 'one' => 1 } 299 | end 300 | 301 | it "evaluates Javascript and returns true" do 302 | result = subject.evaluate_script(%) 303 | result.should === true 304 | end 305 | 306 | it "evaluates Javascript and returns false" do 307 | result = subject.evaluate_script(%) 308 | result.should === false 309 | end 310 | 311 | it "evaluates Javascript and returns an escaped string" do 312 | result = subject.evaluate_script(%<'"'>) 313 | result.should === "\"" 314 | end 315 | 316 | it "evaluates Javascript with multiple lines" do 317 | result = subject.evaluate_script("[1,\n2]") 318 | result.should == [1, 2] 319 | end 320 | 321 | it "executes Javascript" do 322 | subject.execute_script(%) 323 | subject.find("//p[contains(., 'yo')]").should_not be_empty 324 | end 325 | 326 | it "raises an error for failing Javascript" do 327 | expect { subject.execute_script(%) }. 328 | to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError) 329 | end 330 | 331 | it "doesn't raise an error for Javascript that doesn't return anything" do 332 | lambda { subject.execute_script(%<(function () { "returns nothing" })()>) }. 333 | should_not raise_error 334 | end 335 | 336 | it "returns a node's tag name" do 337 | subject.find("//p").first.tag_name.should == "p" 338 | end 339 | 340 | it "reads disabled property" do 341 | subject.find("//input").first.should be_disabled 342 | end 343 | 344 | it "reads checked property" do 345 | subject.find("//input[@id='checktest']").first.should be_checked 346 | end 347 | 348 | it "finds visible elements" do 349 | subject.find("//p").first.should be_visible 350 | subject.find("//*[@id='invisible']").first.should_not be_visible 351 | end 352 | end 353 | 354 | context "console messages app" do 355 | 356 | before(:all) do 357 | @app = lambda do |env| 358 | body = <<-HTML 359 | 360 | 361 | 362 | 363 | 368 | 369 | 370 | HTML 371 | [200, 372 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 373 | [body]] 374 | end 375 | end 376 | 377 | it "collects messages logged to the console" do 378 | subject.console_messages.first.should include :source, :message => "hello", :line_number => 6 379 | subject.console_messages.length.should eq 3 380 | end 381 | 382 | it "logs errors to the console" do 383 | subject.error_messages.length.should eq 1 384 | end 385 | 386 | end 387 | 388 | context "form app" do 389 | before(:all) do 390 | @app = lambda do |env| 391 | body = <<-HTML 392 | 393 |
394 | 395 | 396 | 397 | 398 | 399 | 403 | 412 | 413 | 414 | 415 |
416 | 417 | HTML 418 | [200, 419 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 420 | [body]] 421 | end 422 | end 423 | 424 | it "returns a textarea's value" do 425 | subject.find("//textarea").first.value.should == "what a wonderful area for text" 426 | end 427 | 428 | it "returns a text input's value" do 429 | subject.find("//input").first.value.should == "bar" 430 | end 431 | 432 | it "returns a select's value" do 433 | subject.find("//select").first.value.should == "Capybara" 434 | end 435 | 436 | it "sets an input's value" do 437 | input = subject.find("//input").first 438 | input.set("newvalue") 439 | input.value.should == "newvalue" 440 | end 441 | 442 | it "sets an input's value greater than the max length" do 443 | input = subject.find("//input[@name='maxlength_foo']").first 444 | input.set("allegories (poems)") 445 | input.value.should == "allegories" 446 | end 447 | 448 | it "sets an input's value equal to the max length" do 449 | input = subject.find("//input[@name='maxlength_foo']").first 450 | input.set("allegories") 451 | input.value.should == "allegories" 452 | end 453 | 454 | it "sets an input's value less than the max length" do 455 | input = subject.find("//input[@name='maxlength_foo']").first 456 | input.set("poems") 457 | input.value.should == "poems" 458 | end 459 | 460 | it "sets an input's nil value" do 461 | input = subject.find("//input").first 462 | input.set(nil) 463 | input.value.should == "" 464 | end 465 | 466 | it "sets a select's value" do 467 | select = subject.find("//select").first 468 | select.set("Monkey") 469 | select.value.should == "Monkey" 470 | end 471 | 472 | it "sets a textarea's value" do 473 | textarea = subject.find("//textarea").first 474 | textarea.set("newvalue") 475 | textarea.value.should == "newvalue" 476 | end 477 | 478 | let(:monkey_option) { subject.find("//option[@id='select-option-monkey']").first } 479 | let(:capybara_option) { subject.find("//option[@id='select-option-capybara']").first } 480 | let(:animal_select) { subject.find("//select[@name='animal']").first } 481 | let(:apple_option) { subject.find("//option[@id='topping-apple']").first } 482 | let(:banana_option) { subject.find("//option[@id='topping-banana']").first } 483 | let(:cherry_option) { subject.find("//option[@id='topping-cherry']").first } 484 | let(:toppings_select) { subject.find("//select[@name='toppings']").first } 485 | let(:reset_button) { subject.find("//button[@type='reset']").first } 486 | 487 | context "a select element's selection has been changed" do 488 | before do 489 | animal_select.value.should == "Capybara" 490 | monkey_option.select_option 491 | end 492 | 493 | it "returns the new selection" do 494 | animal_select.value.should == "Monkey" 495 | end 496 | 497 | it "does not modify the selected attribute of a new selection" do 498 | monkey_option['selected'].should be_empty 499 | end 500 | 501 | it "returns the old value when a reset button is clicked" do 502 | reset_button.click 503 | 504 | animal_select.value.should == "Capybara" 505 | end 506 | end 507 | 508 | context "a multi-select element's option has been unselected" do 509 | before do 510 | toppings_select.value.should include("Apple", "Banana", "Cherry") 511 | 512 | apple_option.unselect_option 513 | end 514 | 515 | it "does not return the deselected option" do 516 | toppings_select.value.should_not include("Apple") 517 | end 518 | 519 | it "returns the deselected option when a reset button is clicked" do 520 | reset_button.click 521 | 522 | toppings_select.value.should include("Apple", "Banana", "Cherry") 523 | end 524 | end 525 | 526 | it "reselects an option in a multi-select" do 527 | apple_option.unselect_option 528 | banana_option.unselect_option 529 | cherry_option.unselect_option 530 | 531 | toppings_select.value.should == [] 532 | 533 | apple_option.select_option 534 | banana_option.select_option 535 | cherry_option.select_option 536 | 537 | toppings_select.value.should include("Apple", "Banana", "Cherry") 538 | end 539 | 540 | let(:checked_box) { subject.find("//input[@name='checkedbox']").first } 541 | let(:unchecked_box) { subject.find("//input[@name='uncheckedbox']").first } 542 | 543 | it "knows a checked box is checked" do 544 | checked_box['checked'].should be_true 545 | end 546 | 547 | it "knows a checked box is checked using checked?" do 548 | checked_box.should be_checked 549 | end 550 | 551 | it "knows an unchecked box is unchecked" do 552 | unchecked_box['checked'].should_not be_true 553 | end 554 | 555 | it "knows an unchecked box is unchecked using checked?" do 556 | unchecked_box.should_not be_checked 557 | end 558 | 559 | it "checks an unchecked box" do 560 | unchecked_box.set(true) 561 | unchecked_box.should be_checked 562 | end 563 | 564 | it "unchecks a checked box" do 565 | checked_box.set(false) 566 | checked_box.should_not be_checked 567 | end 568 | 569 | it "leaves a checked box checked" do 570 | checked_box.set(true) 571 | checked_box.should be_checked 572 | end 573 | 574 | it "leaves an unchecked box unchecked" do 575 | unchecked_box.set(false) 576 | unchecked_box.should_not be_checked 577 | end 578 | 579 | let(:enabled_input) { subject.find("//input[@name='foo']").first } 580 | let(:disabled_input) { subject.find("//input[@id='disabled_input']").first } 581 | 582 | it "knows a disabled input is disabled" do 583 | disabled_input['disabled'].should be_true 584 | end 585 | 586 | it "knows a not disabled input is not disabled" do 587 | enabled_input['disabled'].should_not be_true 588 | end 589 | end 590 | 591 | context "dom events" do 592 | before(:all) do 593 | @app = lambda do |env| 594 | body = <<-HTML 595 | 596 | 597 | Link 598 |
    599 | 615 | 616 | HTML 617 | [200, 618 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 619 | [body]] 620 | end 621 | end 622 | 623 | it "triggers mouse events" do 624 | subject.find("//a").first.click 625 | subject.find("//li").map(&:text).should == %w(mousedown mouseup click) 626 | end 627 | end 628 | 629 | context "form events app" do 630 | before(:all) do 631 | @app = lambda do |env| 632 | body = <<-HTML 633 | 634 |
    635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 |
    646 |
      647 | 670 | 671 | HTML 672 | [200, 673 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 674 | [body]] 675 | end 676 | end 677 | 678 | let(:newtext) { 'newvalue' } 679 | 680 | let(:keyevents) do 681 | (%w{focus} + 682 | newtext.length.times.collect { %w{keydown keypress keyup input} } + 683 | %w{change blur}).flatten 684 | end 685 | 686 | %w(email number password search tel text url).each do | field_type | 687 | it "triggers text input events on inputs of type #{field_type}" do 688 | subject.find("//input[@type='#{field_type}']").first.set(newtext) 689 | subject.find("//li").map(&:text).should == keyevents 690 | end 691 | end 692 | 693 | it "triggers textarea input events" do 694 | subject.find("//textarea").first.set(newtext) 695 | subject.find("//li").map(&:text).should == keyevents 696 | end 697 | 698 | it "triggers radio input events" do 699 | subject.find("//input[@type='radio']").first.set(true) 700 | subject.find("//li").map(&:text).should == %w(mousedown mouseup change click) 701 | end 702 | 703 | it "triggers checkbox events" do 704 | subject.find("//input[@type='checkbox']").first.set(true) 705 | subject.find("//li").map(&:text).should == %w(mousedown mouseup change click) 706 | end 707 | end 708 | 709 | context "mouse app" do 710 | before(:all) do 711 | @app =lambda do |env| 712 | body = <<-HTML 713 | 714 |
      Change me
      715 |
      Push me
      716 |
      Release me
      717 |
      718 | 722 |
      723 | 741 | Next 742 | 743 | HTML 744 | [200, 745 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 746 | [body]] 747 | end 748 | end 749 | 750 | it "clicks an element" do 751 | subject.find("//a").first.click 752 | subject.current_url =~ %r{/next$} 753 | end 754 | 755 | it "fires a mouse event" do 756 | subject.find("//*[@id='mouseup']").first.trigger("mouseup") 757 | subject.find("//*[@class='triggered']").should_not be_empty 758 | end 759 | 760 | it "fires a non-mouse event" do 761 | subject.find("//*[@id='change']").first.trigger("change") 762 | subject.find("//*[@class='triggered']").should_not be_empty 763 | end 764 | 765 | it "fires a change on select" do 766 | select = subject.find("//select").first 767 | select.value.should == "1" 768 | option = subject.find("//option[@id='option-2']").first 769 | option.select_option 770 | select.value.should == "2" 771 | subject.find("//select[@class='triggered']").should_not be_empty 772 | end 773 | 774 | it "fires drag events" do 775 | draggable = subject.find("//*[@id='mousedown']").first 776 | container = subject.find("//*[@id='mouseup']").first 777 | 778 | draggable.drag_to(container) 779 | 780 | subject.find("//*[@class='triggered']").size.should == 1 781 | end 782 | end 783 | 784 | context "nesting app" do 785 | before(:all) do 786 | @app = lambda do |env| 787 | body = <<-HTML 788 | 789 |
      790 |
      Expected
      791 |
      792 |
      Unexpected
      793 | 794 | HTML 795 | [200, 796 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 797 | [body]] 798 | end 799 | end 800 | 801 | it "evaluates nested xpath expressions" do 802 | parent = subject.find("//*[@id='parent']").first 803 | parent.find("./*[@class='find']").map(&:text).should == %w(Expected) 804 | end 805 | end 806 | 807 | context "slow app" do 808 | before(:all) do 809 | @result = "" 810 | @app = lambda do |env| 811 | if env["PATH_INFO"] == "/result" 812 | sleep(0.5) 813 | @result << "finished" 814 | end 815 | body = %{Go} 816 | [200, 817 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 818 | [body]] 819 | end 820 | end 821 | 822 | it "waits for a request to load" do 823 | subject.find("//a").first.click 824 | @result.should == "finished" 825 | end 826 | end 827 | 828 | context "error app" do 829 | before(:all) do 830 | @app = lambda do |env| 831 | if env['PATH_INFO'] == "/error" 832 | [404, {}, []] 833 | else 834 | body = <<-HTML 835 | 836 |
      837 | 838 | HTML 839 | [200, 840 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 841 | [body]] 842 | end 843 | end 844 | end 845 | 846 | it "raises a webkit error for the requested url" do 847 | expect { 848 | subject.find("//input").first.click 849 | wait_for_error_to_complete 850 | subject.find("//body") 851 | }. 852 | to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, %r{/error}) 853 | end 854 | 855 | def wait_for_error_to_complete 856 | sleep(0.5) 857 | end 858 | end 859 | 860 | context "slow error app" do 861 | before(:all) do 862 | @app = lambda do |env| 863 | if env['PATH_INFO'] == "/error" 864 | body = "error" 865 | sleep(1) 866 | [304, {}, []] 867 | else 868 | body = <<-HTML 869 | 870 |
      871 |

      hello

      872 | 873 | HTML 874 | [200, 875 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 876 | [body]] 877 | end 878 | end 879 | end 880 | 881 | it "raises a webkit error and then continues" do 882 | subject.find("//input").first.click 883 | expect { subject.find("//p") }.to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError) 884 | subject.visit("/") 885 | subject.find("//p").first.text.should == "hello" 886 | end 887 | end 888 | 889 | context "popup app" do 890 | before(:all) do 891 | @app = lambda do |env| 892 | body = <<-HTML 893 | 894 | 899 |

      success

      900 | 901 | HTML 902 | sleep(0.5) 903 | [200, 904 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 905 | [body]] 906 | end 907 | end 908 | 909 | it "doesn't crash from alerts" do 910 | subject.find("//p").first.text.should == "success" 911 | end 912 | end 913 | 914 | context "custom header" do 915 | before(:all) do 916 | @app = lambda do |env| 917 | body = <<-HTML 918 | 919 |

      #{env['HTTP_USER_AGENT']}

      920 |

      #{env['HTTP_X_CAPYBARA_WEBKIT_HEADER']}

      921 |

      #{env['HTTP_ACCEPT']}

      922 | / 923 | 924 | HTML 925 | [200, 926 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 927 | [body]] 928 | end 929 | end 930 | 931 | before do 932 | subject.header('user-agent', 'capybara-webkit/custom-user-agent') 933 | subject.header('x-capybara-webkit-header', 'x-capybara-webkit-header') 934 | subject.header('accept', 'text/html') 935 | subject.visit('/') 936 | end 937 | 938 | it "can set user_agent" do 939 | subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent' 940 | subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent' 941 | end 942 | 943 | it "keep user_agent in next page" do 944 | subject.find("//a").first.click 945 | subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent' 946 | subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent' 947 | end 948 | 949 | it "can set custom header" do 950 | subject.find('id("x-capybara-webkit-header")').first.text.should == 'x-capybara-webkit-header' 951 | end 952 | 953 | it "can set Accept header" do 954 | subject.find('id("accept")').first.text.should == 'text/html' 955 | end 956 | 957 | it "can reset all custom header" do 958 | subject.reset! 959 | subject.visit('/') 960 | subject.find('id("user-agent")').first.text.should_not == 'capybara-webkit/custom-user-agent' 961 | subject.evaluate_script('navigator.userAgent').should_not == 'capybara-webkit/custom-user-agent' 962 | subject.find('id("x-capybara-webkit-header")').first.text.should be_empty 963 | subject.find('id("accept")').first.text.should_not == 'text/html' 964 | end 965 | end 966 | 967 | context "no response app" do 968 | before(:all) do 969 | @app = lambda do |env| 970 | body = <<-HTML 971 | 972 |
      973 | 974 | HTML 975 | [200, 976 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 977 | [body]] 978 | end 979 | end 980 | 981 | it "raises a webkit error for the requested url" do 982 | make_the_server_go_away 983 | expect { 984 | subject.find("//body") 985 | }. 986 | to raise_error(Capybara::Driver::Webkit::WebkitNoResponseError, %r{response}) 987 | make_the_server_come_back 988 | end 989 | 990 | def make_the_server_come_back 991 | subject.browser.instance_variable_get(:@connection).unstub!(:gets) 992 | subject.browser.instance_variable_get(:@connection).unstub!(:puts) 993 | subject.browser.instance_variable_get(:@connection).unstub!(:print) 994 | end 995 | 996 | def make_the_server_go_away 997 | subject.browser.instance_variable_get(:@connection).stub!(:gets).and_return(nil) 998 | subject.browser.instance_variable_get(:@connection).stub!(:puts) 999 | subject.browser.instance_variable_get(:@connection).stub!(:print) 1000 | end 1001 | end 1002 | 1003 | context "custom font app" do 1004 | before(:all) do 1005 | @app = lambda do |env| 1006 | body = <<-HTML 1007 | 1008 | 1009 | 1012 | 1013 | 1014 |

      Hello

      1015 | 1016 | 1017 | HTML 1018 | [200, 1019 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1020 | [body]] 1021 | end 1022 | end 1023 | 1024 | it "ignores custom fonts" do 1025 | font_family = subject.evaluate_script(<<-SCRIPT) 1026 | var element = document.getElementById("text"); 1027 | element.ownerDocument.defaultView.getComputedStyle(element, null).getPropertyValue("font-family"); 1028 | SCRIPT 1029 | font_family.should == "Arial" 1030 | end 1031 | end 1032 | 1033 | context "cookie-based app" do 1034 | before(:all) do 1035 | @cookie = 'cookie=abc; domain=127.0.0.1; path=/' 1036 | @app = lambda do |env| 1037 | request = ::Rack::Request.new(env) 1038 | 1039 | body = <<-HTML 1040 | 1041 | 1042 | 1043 | HTML 1044 | [200, 1045 | { 'Content-Type' => 'text/html; charset=UTF-8', 1046 | 'Content-Length' => body.length.to_s, 1047 | 'Set-Cookie' => @cookie, 1048 | }, 1049 | [body]] 1050 | end 1051 | end 1052 | 1053 | def echoed_cookie 1054 | subject.find('id("cookie")').first.text 1055 | end 1056 | 1057 | it "remembers the cookie on second visit" do 1058 | echoed_cookie.should == "" 1059 | subject.visit "/" 1060 | echoed_cookie.should == "abc" 1061 | end 1062 | 1063 | it "uses a custom cookie" do 1064 | subject.browser.set_cookie @cookie 1065 | subject.visit "/" 1066 | echoed_cookie.should == "abc" 1067 | end 1068 | 1069 | it "clears cookies" do 1070 | subject.browser.clear_cookies 1071 | subject.visit "/" 1072 | echoed_cookie.should == "" 1073 | end 1074 | 1075 | it "allows enumeration of cookies" do 1076 | cookies = subject.browser.get_cookies 1077 | 1078 | cookies.size.should == 1 1079 | 1080 | cookie = Hash[cookies[0].split(/\s*;\s*/).map { |x| x.split("=", 2) }] 1081 | cookie["cookie"].should == "abc" 1082 | cookie["domain"].should include "127.0.0.1" 1083 | cookie["path"].should == "/" 1084 | end 1085 | 1086 | it "allows reading access to cookies using a nice syntax" do 1087 | subject.cookies["cookie"].should == "abc" 1088 | end 1089 | end 1090 | 1091 | context "with socket debugger" do 1092 | let(:socket_debugger_class){ Capybara::Driver::Webkit::SocketDebugger } 1093 | let(:browser_with_debugger){ 1094 | connection = Capybara::Driver::Webkit::Connection.new(:socket_class => socket_debugger_class) 1095 | Capybara::Driver::Webkit::Browser.new(connection) 1096 | } 1097 | let(:driver_with_debugger){ Capybara::Driver::Webkit.new(@app, :browser => browser_with_debugger) } 1098 | 1099 | before(:all) do 1100 | @app = lambda do |env| 1101 | body = <<-HTML 1102 | 1103 |
      1104 |
      Expected
      1105 |
      1106 |
      Unexpected
      1107 | 1108 | HTML 1109 | [200, 1110 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1111 | [body]] 1112 | end 1113 | end 1114 | 1115 | it "prints out sent content" do 1116 | socket_debugger_class.any_instance.stub(:received){|content| content } 1117 | sent_content = ['Find', 1, 17, "//*[@id='parent']"] 1118 | socket_debugger_class.any_instance.should_receive(:sent).exactly(sent_content.size).times 1119 | driver_with_debugger.find("//*[@id='parent']") 1120 | end 1121 | 1122 | it "prints out received content" do 1123 | socket_debugger_class.any_instance.stub(:sent) 1124 | socket_debugger_class.any_instance.should_receive(:received).at_least(:once).and_return("ok") 1125 | driver_with_debugger.find("//*[@id='parent']") 1126 | end 1127 | end 1128 | 1129 | context "remove node app" do 1130 | before(:all) do 1131 | @app = lambda do |env| 1132 | body = <<-HTML 1133 | 1134 |
      1135 |

      Hello

      1136 |
      1137 | 1138 | HTML 1139 | [200, 1140 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1141 | [body]] 1142 | end 1143 | end 1144 | 1145 | before { set_automatic_reload false } 1146 | after { set_automatic_reload true } 1147 | 1148 | def set_automatic_reload(value) 1149 | if Capybara.respond_to?(:automatic_reload) 1150 | Capybara.automatic_reload = value 1151 | end 1152 | end 1153 | 1154 | it "allows removed nodes when reloading is disabled" do 1155 | node = subject.find("//p[@id='removeMe']").first 1156 | subject.evaluate_script("document.getElementById('parent').innerHTML = 'Magic'") 1157 | node.text.should == 'Hello' 1158 | end 1159 | end 1160 | 1161 | context "app with a lot of HTML tags" do 1162 | before(:all) do 1163 | @app = lambda do |env| 1164 | body = <<-HTML 1165 | 1166 | 1167 | My eBook 1168 | 1169 | 1170 | 1171 | 1172 |
      1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 |
      ChapterPage
      Intro1
      Chapter 11
      Chapter 21
      1183 |
      1184 | 1185 |

      My first book

      1186 |

      Written by me

      1187 |
      1188 |

      Let's try out XPath

      1189 |

      in capybara-webkit

      1190 |
      1191 | 1192 |

      Chapter 1

      1193 |

      This paragraph is fascinating.

      1194 |

      But not as much as this one.

      1195 | 1196 |

      Chapter 2

      1197 |

      Let's try if we can select this

      1198 | 1199 | 1200 | HTML 1201 | [200, 1202 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1203 | [body]] 1204 | end 1205 | end 1206 | 1207 | it "builds up node paths correctly" do 1208 | cases = { 1209 | "//*[contains(@class, 'author')]" => "/html/head/meta[2]", 1210 | "//*[contains(@class, 'td1')]" => "/html/body/div[@id='toc']/table/thead[@id='head']/tr/td[1]", 1211 | "//*[contains(@class, 'td2')]" => "/html/body/div[@id='toc']/table/tbody/tr[2]/td[2]", 1212 | "//h1" => "/html/body/h1", 1213 | "//*[contains(@class, 'chapter2')]" => "/html/body/h2[2]", 1214 | "//*[contains(@class, 'p1')]" => "/html/body/p[1]", 1215 | "//*[contains(@class, 'p2')]" => "/html/body/div[@id='intro']/p[2]", 1216 | "//*[contains(@class, 'p3')]" => "/html/body/p[3]", 1217 | } 1218 | 1219 | cases.each do |xpath, path| 1220 | nodes = subject.find(xpath) 1221 | nodes.size.should == 1 1222 | nodes[0].path.should == path 1223 | end 1224 | end 1225 | end 1226 | 1227 | context "css overflow app" do 1228 | before(:all) do 1229 | @app = lambda do |env| 1230 | body = <<-HTML 1231 | 1232 | 1233 | 1236 | 1237 | 1238 |
      Overflow
      1239 | 1240 | 1241 | HTML 1242 | [200, 1243 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1244 | [body]] 1245 | end 1246 | end 1247 | 1248 | it "handles overflow hidden" do 1249 | subject.find("//div[@id='overflow']").first.text.should == "Overflow" 1250 | end 1251 | end 1252 | 1253 | context "javascript redirect app" do 1254 | before(:all) do 1255 | @app = lambda do |env| 1256 | if env['PATH_INFO'] == '/redirect' 1257 | body = <<-HTML 1258 | 1259 | 1262 | 1263 | HTML 1264 | else 1265 | body = "

      finished

      " 1266 | end 1267 | [200, 1268 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1269 | [body]] 1270 | end 1271 | end 1272 | 1273 | it "loads a page without error" do 1274 | 10.times do 1275 | subject.visit("/redirect") 1276 | subject.find("//p").first.text.should == "finished" 1277 | end 1278 | end 1279 | end 1280 | 1281 | context "localStorage works" do 1282 | before(:all) do 1283 | @app = lambda do |env| 1284 | body = <<-HTML 1285 | 1286 | 1287 | 1288 | 1298 | 1299 | 1300 | HTML 1301 | [200, 1302 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1303 | [body]] 1304 | end 1305 | end 1306 | 1307 | it "displays the message on subsequent page loads" do 1308 | subject.find("//span[contains(.,'localStorage is enabled')]").should be_empty 1309 | subject.visit "/" 1310 | subject.find("//span[contains(.,'localStorage is enabled')]").should_not be_empty 1311 | end 1312 | end 1313 | 1314 | context "app with a lot of HTML tags" do 1315 | before(:all) do 1316 | @app = lambda do |env| 1317 | body = <<-HTML 1318 | 1319 | 1320 | My eBook 1321 | 1322 | 1323 | 1324 | 1325 |
      1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 |
      ChapterPage
      Intro1
      Chapter 11
      Chapter 21
      1336 |
      1337 | 1338 |

      My first book

      1339 |

      Written by me

      1340 |
      1341 |

      Let's try out XPath

      1342 |

      in capybara-webkit

      1343 |
      1344 | 1345 |

      Chapter 1

      1346 |

      This paragraph is fascinating.

      1347 |

      But not as much as this one.

      1348 | 1349 |

      Chapter 2

      1350 |

      Let's try if we can select this

      1351 | 1352 | 1353 | HTML 1354 | [200, 1355 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1356 | [body]] 1357 | end 1358 | end 1359 | 1360 | it "builds up node paths correctly" do 1361 | cases = { 1362 | "//*[contains(@class, 'author')]" => "/html/head/meta[2]", 1363 | "//*[contains(@class, 'td1')]" => "/html/body/div[@id='toc']/table/thead[@id='head']/tr/td[1]", 1364 | "//*[contains(@class, 'td2')]" => "/html/body/div[@id='toc']/table/tbody/tr[2]/td[2]", 1365 | "//h1" => "/html/body/h1", 1366 | "//*[contains(@class, 'chapter2')]" => "/html/body/h2[2]", 1367 | "//*[contains(@class, 'p1')]" => "/html/body/p[1]", 1368 | "//*[contains(@class, 'p2')]" => "/html/body/div[@id='intro']/p[2]", 1369 | "//*[contains(@class, 'p3')]" => "/html/body/p[3]", 1370 | } 1371 | 1372 | cases.each do |xpath, path| 1373 | nodes = subject.find(xpath) 1374 | nodes.size.should == 1 1375 | nodes[0].path.should == path 1376 | end 1377 | end 1378 | end 1379 | 1380 | context "form app with server-side handler" do 1381 | before(:all) do 1382 | @app = lambda do |env| 1383 | if env["REQUEST_METHOD"] == "POST" 1384 | body = "

      Congrats!

      " 1385 | else 1386 | body = <<-HTML 1387 | 1388 | Form 1389 | 1390 |
      1391 | 1392 | 1393 |
      1394 | 1395 | 1396 | HTML 1397 | end 1398 | [200, 1399 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1400 | [body]] 1401 | end 1402 | end 1403 | 1404 | it "submits a form without clicking" do 1405 | subject.find("//form")[0].submit 1406 | subject.body.should include "Congrats" 1407 | end 1408 | end 1409 | 1410 | def key_app_body(event) 1411 | body = <<-HTML 1412 | 1413 | Form 1414 | 1415 |
      1416 |
      1417 |
      1418 | 1419 | 1431 | 1432 | 1433 | HTML 1434 | body 1435 | end 1436 | 1437 | def charCode_for(character) 1438 | subject.find("//input")[0].set(character) 1439 | subject.find("//div[@id='charcode_value']")[0].text 1440 | end 1441 | 1442 | def keyCode_for(character) 1443 | subject.find("//input")[0].set(character) 1444 | subject.find("//div[@id='keycode_value']")[0].text 1445 | end 1446 | 1447 | def which_for(character) 1448 | subject.find("//input")[0].set(character) 1449 | subject.find("//div[@id='which_value']")[0].text 1450 | end 1451 | 1452 | context "keypress app" do 1453 | before(:all) do 1454 | @app = lambda do |env| 1455 | body = key_app_body("keypress") 1456 | [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]] 1457 | end 1458 | end 1459 | 1460 | it "returns the charCode for the keypressed" do 1461 | charCode_for("a").should == "97" 1462 | charCode_for("A").should == "65" 1463 | charCode_for("\r").should == "13" 1464 | charCode_for(",").should == "44" 1465 | charCode_for("<").should == "60" 1466 | charCode_for("0").should == "48" 1467 | end 1468 | 1469 | it "returns the keyCode for the keypressed" do 1470 | keyCode_for("a").should == "97" 1471 | keyCode_for("A").should == "65" 1472 | keyCode_for("\r").should == "13" 1473 | keyCode_for(",").should == "44" 1474 | keyCode_for("<").should == "60" 1475 | keyCode_for("0").should == "48" 1476 | end 1477 | 1478 | it "returns the which for the keypressed" do 1479 | which_for("a").should == "97" 1480 | which_for("A").should == "65" 1481 | which_for("\r").should == "13" 1482 | which_for(",").should == "44" 1483 | which_for("<").should == "60" 1484 | which_for("0").should == "48" 1485 | end 1486 | end 1487 | 1488 | shared_examples "a keyupdown app" do 1489 | it "returns a 0 charCode for the event" do 1490 | charCode_for("a").should == "0" 1491 | charCode_for("A").should == "0" 1492 | charCode_for("\r").should == "0" 1493 | charCode_for(",").should == "0" 1494 | charCode_for("<").should == "0" 1495 | charCode_for("0").should == "0" 1496 | end 1497 | 1498 | it "returns the keyCode for the event" do 1499 | keyCode_for("a").should == "65" 1500 | keyCode_for("A").should == "65" 1501 | keyCode_for("\r").should == "13" 1502 | keyCode_for(",").should == "188" 1503 | keyCode_for("<").should == "188" 1504 | keyCode_for("0").should == "48" 1505 | end 1506 | 1507 | it "returns the which for the event" do 1508 | which_for("a").should == "65" 1509 | which_for("A").should == "65" 1510 | which_for("\r").should == "13" 1511 | which_for(",").should == "188" 1512 | which_for("<").should == "188" 1513 | which_for("0").should == "48" 1514 | end 1515 | end 1516 | 1517 | context "keydown app" do 1518 | before(:all) do 1519 | @app = lambda do |env| 1520 | body = key_app_body("keydown") 1521 | [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]] 1522 | end 1523 | end 1524 | it_behaves_like "a keyupdown app" 1525 | end 1526 | 1527 | context "keyup app" do 1528 | before(:all) do 1529 | @app = lambda do |env| 1530 | body = key_app_body("keyup") 1531 | [200, { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, [body]] 1532 | end 1533 | end 1534 | 1535 | it_behaves_like "a keyupdown app" 1536 | end 1537 | 1538 | context "null byte app" do 1539 | before(:all) do 1540 | @app = lambda do |env| 1541 | body = "Hello\0World" 1542 | [200, 1543 | { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, 1544 | [body]] 1545 | end 1546 | end 1547 | 1548 | it "should include all the bytes in the source" do 1549 | subject.source.should == "Hello\0World" 1550 | end 1551 | end 1552 | end 1553 | --------------------------------------------------------------------------------